/* Directory hashing for GNU Make.
Copyright (C) 1988-2020 Free Software Foundation, Inc.
This file is part of GNU Make.

GNU Make is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3 of the License, or (at your option) any later
version.

GNU Make is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE.  See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License along with
this program.  If not, see <http://www.gnu.org/licenses/>.  */

#include "third_party/make/makeint.inc"
/**/
#include "libc/calls/struct/dirent.h"
#include "third_party/make/dep.h"
#include "third_party/make/filedef.h"
#include "third_party/make/hash.h"
#include "third_party/musl/glob.h"
/* clang-format off */

#ifndef __ptr_t
#define __ptr_t void *
#endif
#define gl_readdir  __dummy2[1]
#define gl_opendir  __dummy2[2]
#define gl_closedir __dummy2[0]
#define gl_lstat    __dummy2[3]
#define gl_stat     __dummy2[4]

#ifdef HAVE_DIRENT_H
#define NAMLEN(dirent) strlen((dirent)->d_name)
#else
#define dirent         direct
#define NAMLEN(dirent) (dirent)->d_namlen
#endif

/* In GNU systems, <dirent.h> defines this macro for us.  */
#ifdef _D_NAMLEN
#undef NAMLEN
#define NAMLEN(d) _D_NAMLEN(d)
#endif

#if (defined(POSIX) || defined(VMS) || defined(WINDOWS32)) && \
    !defined(__GNU_LIBRARY__)
/* Posix does not require that the d_ino field be present, and some
   systems do not provide it. */
#define REAL_DIR_ENTRY(dp) 1
#define FAKE_DIR_ENTRY(dp)
#else
#define REAL_DIR_ENTRY(dp) (dp->d_ino != 0)
#define FAKE_DIR_ENTRY(dp) (dp->d_ino = 1)
#endif /* POSIX */

#ifdef HAVE_CASE_INSENSITIVE_FS
static const char *downcase(const char *filename) {
  static PATH_VAR(new_filename);
  char *df;

  if (filename == 0) return 0;

  df = new_filename;
  while (*filename != '\0') {
    *df++ = tolower((unsigned char)*filename);
    ++filename;
  }

  *df = 0;

  return new_filename;
}
#endif /* HAVE_CASE_INSENSITIVE_FS */

/* Hash table of directories.  */

#ifndef DIRECTORY_BUCKETS
#define DIRECTORY_BUCKETS 199
#endif

struct directory_contents {
  dev_t dev; /* Device and inode numbers of this dir.  */
#ifdef WINDOWS32
  /* Inode means nothing on WINDOWS32. Even file key information is
   * unreliable because it is random per file open and undefined for remote
   * filesystems. The most unique attribute I can come up with is the fully
   * qualified name of the directory. Beware though, this is also
   * unreliable. I'm open to suggestion on a better way to emulate inode.  */
  char *path_key;
  time_t ctime;
  time_t mtime; /* controls check for stale directory cache */
  int fs_flags; /* FS_FAT, FS_NTFS, ... */
#define FS_FAT     0x1
#define FS_NTFS    0x2
#define FS_UNKNOWN 0x4
#else
  ino_t ino;
#endif /* WINDOWS32 */
  struct hash_table dirfiles; /* Files in this directory.  */
  DIR *dirstream;             /* Stream reading this directory.  */
};

static unsigned long directory_contents_hash_1(const void *key_0) {
  const struct directory_contents *key = key_0;
  unsigned long hash;

#ifdef WINDOWS32
  hash = 0;
  ISTRING_HASH_1(key->path_key, hash);
  hash ^= ((unsigned int)key->dev << 4) ^ (unsigned int)key->ctime;
#else
  hash = ((unsigned int)key->dev << 4) ^ (unsigned int)key->ino;
#endif /* WINDOWS32 */
  return hash;
}

static unsigned long directory_contents_hash_2(const void *key_0) {
  const struct directory_contents *key = key_0;
  unsigned long hash;

#ifdef WINDOWS32
  hash = 0;
  ISTRING_HASH_2(key->path_key, hash);
  hash ^= ((unsigned int)key->dev << 4) ^ (unsigned int)~key->ctime;
#else
  hash = ((unsigned int)key->dev << 4) ^ (unsigned int)~key->ino;
#endif /* WINDOWS32 */

  return hash;
}

/* Sometimes it's OK to use subtraction to get this value:
     result = X - Y;
   But, if we're not sure of the type of X and Y they may be too large for an
   int (on a 64-bit system for example).  So, use ?: instead.
   See Savannah bug #15534.

   NOTE!  This macro has side-effects!
*/

#define MAKECMP(_x, _y) ((_x) < (_y) ? -1 : ((_x) == (_y) ? 0 : 1))

static int directory_contents_hash_cmp(const void *xv, const void *yv) {
  const struct directory_contents *x = xv;
  const struct directory_contents *y = yv;
  int result;

#ifdef WINDOWS32
  ISTRING_COMPARE(x->path_key, y->path_key, result);
  if (result) return result;
  result = MAKECMP(x->ctime, y->ctime);
  if (result) return result;
#else
  result = MAKECMP(x->ino, y->ino);
  if (result) return result;
#endif /* WINDOWS32 */

  return MAKECMP(x->dev, y->dev);
}

/* Table of directory contents hashed by device and inode number.  */
static struct hash_table directory_contents;

struct directory {
  const char *name; /* Name of the directory.  */

  /* The directory's contents.  This data may be shared by several
     entries in the hash table, which refer to the same directory
     (identified uniquely by 'dev' and 'ino') under different names.  */
  struct directory_contents *contents;
};

static unsigned long directory_hash_1(const void *key) {
  return_ISTRING_HASH_1(((const struct directory *)key)->name);
}

static unsigned long directory_hash_2(const void *key) {
  return_ISTRING_HASH_2(((const struct directory *)key)->name);
}

static int directory_hash_cmp(const void *x, const void *y) {
  return_ISTRING_COMPARE(((const struct directory *)x)->name,
                         ((const struct directory *)y)->name);
}

/* Table of directories hashed by name.  */
static struct hash_table directories;

/* Never have more than this many directories open at once.  */

#define MAX_OPEN_DIRECTORIES 10

static unsigned int open_directories = 0;

/* Hash table of files in each directory.  */

struct dirfile {
  const char *name; /* Name of the file.  */
  size_t length;
  short impossible; /* This file is impossible.  */
  unsigned char type;
};

static unsigned long dirfile_hash_1(const void *key) {
  return_ISTRING_HASH_1(((struct dirfile const *)key)->name);
}

static unsigned long dirfile_hash_2(const void *key) {
  return_ISTRING_HASH_2(((struct dirfile const *)key)->name);
}

static int dirfile_hash_cmp(const void *xv, const void *yv) {
  const struct dirfile *x = xv;
  const struct dirfile *y = yv;
  int result = (int)(x->length - y->length);
  if (result) return result;
  return_ISTRING_COMPARE(x->name, y->name);
}

#ifndef DIRFILE_BUCKETS
#define DIRFILE_BUCKETS 107
#endif

static int dir_contents_file_exists_p(struct directory_contents *dir,
                                      const char *filename);
static struct directory *find_directory(const char *name);

/* Find the directory named NAME and return its 'struct directory'.  */

static struct directory *find_directory(const char *name) {
  struct directory *dir;
  struct directory **dir_slot;
  struct directory dir_key;

  dir_key.name = name;
  dir_slot = (struct directory **)hash_find_slot(&directories, &dir_key);
  dir = *dir_slot;

  if (HASH_VACANT(dir)) {
    /* The directory was not found.  Create a new entry for it.  */
    const char *p = name + strlen(name);
    struct stat st;
    int r;

    dir = xmalloc(sizeof(struct directory));
#if defined(HAVE_CASE_INSENSITIVE_FS) && defined(VMS)
    /* Todo: Why is this only needed on VMS? */
    {
      char *lname = downcase_inplace(xstrdup(name));
      dir->name = strcache_add_len(lname, p - name);
      free(lname);
    }
#else
    dir->name = strcache_add_len(name, p - name);
#endif
    hash_insert_at(&directories, dir, dir_slot);
    /* The directory is not in the name hash table.
       Find its device and inode numbers, and look it up by them.  */

#if defined(WINDOWS32)
    {
      char tem[MAXPATHLEN], *tstart, *tend;

      /* Remove any trailing slashes.  Windows32 stat fails even on
         valid directories if they end in a slash. */
      memcpy(tem, name, p - name + 1);
      tstart = tem;
      if (tstart[1] == ':') tstart += 2;
      for (tend = tem + (p - name - 1);
           tend > tstart && (*tend == '/' || *tend == '\\'); tend--)
        *tend = '\0';

      r = stat(tem, &st);
    }
#else
    EINTRLOOP(r, stat(name, &st));
#endif

    if (r < 0) {
      /* Couldn't stat the directory.  Mark this by
         setting the 'contents' member to a nil pointer.  */
      dir->contents = 0;
    } else {
      /* Search the contents hash table; device and inode are the key.  */

#ifdef WINDOWS32
      char *w32_path;
#endif
      struct directory_contents *dc;
      struct directory_contents **dc_slot;
      struct directory_contents dc_key;

      dc_key.dev = st.st_dev;
#ifdef WINDOWS32
      dc_key.path_key = w32_path = w32ify(name, 1);
      dc_key.ctime = st.st_ctime;
#else
      dc_key.ino = st.st_ino;
#endif
      dc_slot = (struct directory_contents **)hash_find_slot(
          &directory_contents, &dc_key);
      dc = *dc_slot;

      if (HASH_VACANT(dc)) {
        /* Nope; this really is a directory we haven't seen before.  */
#ifdef WINDOWS32
        char fs_label[BUFSIZ];
        char fs_type[BUFSIZ];
        unsigned long fs_serno;
        unsigned long fs_flags;
        unsigned long fs_len;
#endif
        dc = (struct directory_contents *)xmalloc(
            sizeof(struct directory_contents));

        /* Enter it in the contents hash table.  */
        dc->dev = st.st_dev;
#ifdef WINDOWS32
        dc->path_key = xstrdup(w32_path);
        dc->ctime = st.st_ctime;
        dc->mtime = st.st_mtime;

        /* NTFS is the only WINDOWS32 filesystem that bumps mtime on a
           directory when files are added/deleted from a directory.  */
        w32_path[3] = '\0';
        if (GetVolumeInformation(w32_path, fs_label, sizeof(fs_label),
                                 &fs_serno, &fs_len, &fs_flags, fs_type,
                                 sizeof(fs_type)) == FALSE)
          dc->fs_flags = FS_UNKNOWN;
        else if (!strcmp(fs_type, "FAT"))
          dc->fs_flags = FS_FAT;
        else if (!strcmp(fs_type, "NTFS"))
          dc->fs_flags = FS_NTFS;
        else
          dc->fs_flags = FS_UNKNOWN;
#else
        dc->ino = st.st_ino;
#endif /* WINDOWS32 */
        hash_insert_at(&directory_contents, dc, dc_slot);
        ENULLLOOP(dc->dirstream, opendir(name));
        if (dc->dirstream == 0)
          /* Couldn't open the directory.  Mark this by setting the
             'files' member to a nil pointer.  */
          dc->dirfiles.ht_vec = 0;
        else {
          hash_init(&dc->dirfiles, DIRFILE_BUCKETS, dirfile_hash_1,
                    dirfile_hash_2, dirfile_hash_cmp);
          /* Keep track of how many directories are open.  */
          ++open_directories;
          if (open_directories == MAX_OPEN_DIRECTORIES)
            /* We have too many directories open already.
               Read the entire directory and then close it.  */
            dir_contents_file_exists_p(dc, 0);
        }
      }

      /* Point the name-hashed entry for DIR at its contents data.  */
      dir->contents = dc;
    }
  }

  return dir;
}

/* Return 1 if the name FILENAME is entered in DIR's hash table.
   FILENAME must contain no slashes.  */

static int dir_contents_file_exists_p(struct directory_contents *dir,
                                      const char *filename) {
  struct dirfile *df;
  struct dirent *d;
#ifdef WINDOWS32
  struct stat st;
  int rehash = 0;
#endif

  if (dir == 0 || dir->dirfiles.ht_vec == 0)
    /* The directory could not be stat'd or opened.  */
    return 0;

#ifdef HAVE_CASE_INSENSITIVE_FS
  filename = downcase(filename);
#endif

  if (filename != 0) {
    struct dirfile dirfile_key;

    if (*filename == '\0') {
      /* Checking if the directory exists.  */
      return 1;
    }
    dirfile_key.name = filename;
    dirfile_key.length = strlen(filename);
    df = hash_find_item(&dir->dirfiles, &dirfile_key);
    if (df) return !df->impossible;
  }

  /* The file was not found in the hashed list.
     Try to read the directory further.  */

  if (dir->dirstream == 0) {
#ifdef WINDOWS32
    /*
     * Check to see if directory has changed since last read. FAT
     * filesystems force a rehash always as mtime does not change
     * on directories (ugh!).
     */
    if (dir->path_key) {
      if ((dir->fs_flags & FS_FAT) != 0) {
        dir->mtime = time((time_t *)0);
        rehash = 1;
      } else if (stat(dir->path_key, &st) == 0 && st.st_mtime > dir->mtime) {
        /* reset date stamp to show most recent re-process.  */
        dir->mtime = st.st_mtime;
        rehash = 1;
      }

      /* If it has been already read in, all done.  */
      if (!rehash) return 0;

      /* make sure directory can still be opened; if not return.  */
      dir->dirstream = opendir(dir->path_key);
      if (!dir->dirstream) return 0;
    } else
#endif
      /* The directory has been all read in.  */
      return 0;
  }

  while (1) {
    /* Enter the file in the hash table.  */
    size_t len;
    struct dirfile dirfile_key;
    struct dirfile **dirfile_slot;

    ENULLLOOP(d, readdir(dir->dirstream));
    if (d == 0) {
      if (errno) pfatal_with_name("INTERNAL: readdir");
      break;
    }

    if (!REAL_DIR_ENTRY(d)) continue;

    len = NAMLEN(d);
    dirfile_key.name = d->d_name;
    dirfile_key.length = len;
    dirfile_slot =
        (struct dirfile **)hash_find_slot(&dir->dirfiles, &dirfile_key);
#ifdef WINDOWS32
    /*
     * If re-reading a directory, don't cache files that have
     * already been discovered.
     */
    if (!rehash || HASH_VACANT(*dirfile_slot))
#endif
    {
      df = xmalloc(sizeof(struct dirfile));
#if defined(HAVE_CASE_INSENSITIVE_FS) && defined(VMS)
      /* TODO: Why is this only needed on VMS? */
      df->name = strcache_add_len(downcase_inplace(d->d_name), len);
#else
      df->name = strcache_add_len(d->d_name, len);
#endif
#ifdef HAVE_STRUCT_DIRENT_D_TYPE
      df->type = d->d_type;
#endif
      df->length = len;
      df->impossible = 0;
      hash_insert_at(&dir->dirfiles, df, dirfile_slot);
    }
    /* Check if the name matches the one we're searching for.  */
    if (filename != 0 && patheq(d->d_name, filename)) return 1;
  }

  /* If the directory has been completely read in,
     close the stream and reset the pointer to nil.  */
  if (d == 0) {
    --open_directories;
    closedir(dir->dirstream);
    dir->dirstream = 0;
  }
  return 0;
}

/* Return 1 if the name FILENAME in directory DIRNAME
   is entered in the dir hash table.
   FILENAME must contain no slashes.  */

int dir_file_exists_p(const char *dirname, const char *filename) {
  return dir_contents_file_exists_p(find_directory(dirname)->contents,
                                    filename);
}

/* Return 1 if the file named NAME exists.  */

int file_exists_p(const char *name) {
  const char *dirend;
  const char *dirname;
  const char *slash;

#ifndef NO_ARCHIVES
  if (ar_name(name)) return ar_member_date(name) != (time_t)-1;
#endif

  dirend = strrchr(name, '/');
#ifdef HAVE_DOS_PATHS
  /* Forward and backslashes might be mixed.  We need the rightmost one.  */
  {
    const char *bslash = strrchr(name, '\\');
    if (!dirend || bslash > dirend) dirend = bslash;
    /* The case of "d:file".  */
    if (!dirend && name[0] && name[1] == ':') dirend = name + 1;
  }
#endif /* HAVE_DOS_PATHS */
  if (dirend == 0) return dir_file_exists_p(".", name);

  slash = dirend;
  if (dirend == name)
    dirname = "/";
  else {
    char *p;
#ifdef HAVE_DOS_PATHS
    /* d:/ and d: are *very* different...  */
    if (dirend < name + 3 && name[1] == ':' &&
        (*dirend == '/' || *dirend == '\\' || *dirend == ':'))
      dirend++;
#endif
    p = alloca(dirend - name + 1);
    memcpy(p, name, dirend - name);
    p[dirend - name] = '\0';
    dirname = p;
  }
  slash++;
  return dir_file_exists_p(dirname, slash);
}

/* Mark FILENAME as 'impossible' for 'file_impossible_p'.
   This means an attempt has been made to search for FILENAME
   as an intermediate file, and it has failed.  */

void file_impossible(const char *filename) {
  const char *dirend;
  const char *p = filename;
  struct directory *dir;
  struct dirfile *new;

  dirend = strrchr(p, '/');
#ifdef HAVE_DOS_PATHS
  /* Forward and backslashes might be mixed.  We need the rightmost one.  */
  {
    const char *bslash = strrchr(p, '\\');
    if (!dirend || bslash > dirend) dirend = bslash;
    /* The case of "d:file".  */
    if (!dirend && p[0] && p[1] == ':') dirend = p + 1;
  }
#endif /* HAVE_DOS_PATHS */
  if (dirend == 0)
    dir = find_directory(".");
  else {
    const char *dirname;
    const char *slash = dirend;
    if (dirend == p)
      dirname = "/";
    else {
      char *cp;
#ifdef HAVE_DOS_PATHS
      /* d:/ and d: are *very* different...  */
      if (dirend < p + 3 && p[1] == ':' &&
          (*dirend == '/' || *dirend == '\\' || *dirend == ':'))
        dirend++;
#endif
      cp = alloca(dirend - p + 1);
      memcpy(cp, p, dirend - p);
      cp[dirend - p] = '\0';
      dirname = cp;
    }
    dir = find_directory(dirname);
    filename = p = slash + 1;
  }

  if (dir->contents == 0)
    /* The directory could not be stat'd.  We allocate a contents
       structure for it, but leave it out of the contents hash table.  */
    dir->contents = xcalloc(sizeof(struct directory_contents));

  if (dir->contents->dirfiles.ht_vec == 0) {
    hash_init(&dir->contents->dirfiles, DIRFILE_BUCKETS, dirfile_hash_1,
              dirfile_hash_2, dirfile_hash_cmp);
  }

  /* Make a new entry and put it in the table.  */

  new = xmalloc(sizeof(struct dirfile));
  new->length = strlen(filename);
#if defined(HAVE_CASE_INSENSITIVE_FS) && defined(VMS)
  /* todo: Why is this only needed on VMS? */
  new->name = strcache_add_len(downcase(filename), new->length);
#else
  new->name = strcache_add_len(filename, new->length);
#endif
  new->impossible = 1;
  hash_insert(&dir->contents->dirfiles, new);
}

/* Return nonzero if FILENAME has been marked impossible.  */

int file_impossible_p(const char *filename) {
  const char *dirend;
  struct directory_contents *dir;
  struct dirfile *dirfile;
  struct dirfile dirfile_key;

  dirend = strrchr(filename, '/');
#ifdef HAVE_DOS_PATHS
  /* Forward and backslashes might be mixed.  We need the rightmost one.  */
  {
    const char *bslash = strrchr(filename, '\\');
    if (!dirend || bslash > dirend) dirend = bslash;
    /* The case of "d:file".  */
    if (!dirend && filename[0] && filename[1] == ':') dirend = filename + 1;
  }
#endif /* HAVE_DOS_PATHS */
  if (dirend == 0)
    dir = find_directory(".")->contents;
  else {
    const char *dirname;
    const char *slash = dirend;
    if (dirend == filename)
      dirname = "/";
    else {
      char *cp;
#ifdef HAVE_DOS_PATHS
      /* d:/ and d: are *very* different...  */
      if (dirend < filename + 3 && filename[1] == ':' &&
          (*dirend == '/' || *dirend == '\\' || *dirend == ':'))
        dirend++;
#endif
      cp = alloca(dirend - filename + 1);
      memcpy(cp, filename, dirend - filename);
      cp[dirend - filename] = '\0';
      dirname = cp;
    }
    dir = find_directory(dirname)->contents;
    filename = slash + 1;
  }

  if (dir == 0 || dir->dirfiles.ht_vec == 0)
    /* There are no files entered for this directory.  */
    return 0;

#ifdef HAVE_CASE_INSENSITIVE_FS
  filename = downcase(filename);
#endif

  dirfile_key.name = filename;
  dirfile_key.length = strlen(filename);
  dirfile = hash_find_item(&dir->dirfiles, &dirfile_key);
  if (dirfile) return dirfile->impossible;

  return 0;
}

/* Return the already allocated name in the
   directory hash table that matches DIR.  */

const char *dir_name(const char *dir) {
  return find_directory(dir)->name;
}

/* Print the data base of directories.  */

void print_dir_data_base(void) {
  unsigned int files;
  unsigned int impossible;
  struct directory **dir_slot;
  struct directory **dir_end;

  puts(_("\n# Directories\n"));

  files = impossible = 0;

  dir_slot = (struct directory **)directories.ht_vec;
  dir_end = dir_slot + directories.ht_size;
  for (; dir_slot < dir_end; dir_slot++) {
    struct directory *dir = *dir_slot;
    if (!HASH_VACANT(dir)) {
      if (dir->contents == 0)
        printf(_("# %s: could not be stat'd.\n"), dir->name);
      else if (dir->contents->dirfiles.ht_vec == 0) {
        printf(_("# %s (device %ld, inode %ld): could not be opened.\n"),
               dir->name, (long int)dir->contents->dev,
               (long int)dir->contents->ino);
      } else {
        unsigned int f = 0;
        unsigned int im = 0;
        struct dirfile **files_slot;
        struct dirfile **files_end;

        files_slot = (struct dirfile **)dir->contents->dirfiles.ht_vec;
        files_end = files_slot + dir->contents->dirfiles.ht_size;
        for (; files_slot < files_end; files_slot++) {
          struct dirfile *df = *files_slot;
          if (!HASH_VACANT(df)) {
            if (df->impossible)
              ++im;
            else
              ++f;
          }
        }
        printf(_("# %s (device %ld, inode %ld): "), dir->name,
               (long)dir->contents->dev, (long)dir->contents->ino);
        if (f == 0)
          fputs(_("No"), stdout);
        else
          printf("%u", f);
        fputs(_(" files, "), stdout);
        if (im == 0)
          fputs(_("no"), stdout);
        else
          printf("%u", im);
        fputs(_(" impossibilities"), stdout);
        if (dir->contents->dirstream == 0)
          puts(".");
        else
          puts(_(" so far."));
        files += f;
        impossible += im;
      }
    }
  }

  fputs("\n# ", stdout);
  if (files == 0)
    fputs(_("No"), stdout);
  else
    printf("%u", files);
  fputs(_(" files, "), stdout);
  if (impossible == 0)
    fputs(_("no"), stdout);
  else
    printf("%u", impossible);
  printf(_(" impossibilities in %lu directories.\n"), directories.ht_fill);
}

/* Hooks for globbing.  */

/* Structure describing state of iterating through a directory hash table.  */

struct dirstream {
  struct directory_contents *contents; /* The directory being read.  */
  struct dirfile **dirfile_slot;       /* Current slot in table.  */
};

/* Forward declarations.  */
static __ptr_t open_dirstream(const char *);
static struct dirent *read_dirstream(__ptr_t);

static __ptr_t open_dirstream(const char *directory) {
  struct dirstream *new;
  struct directory *dir = find_directory(directory);

  if (dir->contents == 0 || dir->contents->dirfiles.ht_vec == 0)
    /* DIR->contents is nil if the directory could not be stat'd.
       DIR->contents->dirfiles is nil if it could not be opened.  */
    return 0;

  /* Read all the contents of the directory now.  There is no benefit
     in being lazy, since glob will want to see every file anyway.  */

  dir_contents_file_exists_p(dir->contents, 0);

  new = xmalloc(sizeof(struct dirstream));
  new->contents = dir->contents;
  new->dirfile_slot = (struct dirfile **)new->contents->dirfiles.ht_vec;

  return (__ptr_t) new;
}

static struct dirent *read_dirstream(__ptr_t stream) {
  static char *buf;
  static size_t bufsz;

  struct dirstream *const ds = (struct dirstream *)stream;
  struct directory_contents *dc = ds->contents;
  struct dirfile **dirfile_end =
      (struct dirfile **)dc->dirfiles.ht_vec + dc->dirfiles.ht_size;

  while (ds->dirfile_slot < dirfile_end) {
    struct dirfile *df = *ds->dirfile_slot++;
    if (!HASH_VACANT(df) && !df->impossible) {
      /* The glob interface wants a 'struct dirent', so mock one up.  */
      struct dirent *d;
      size_t len = df->length + 1;
      size_t sz = sizeof(*d) - sizeof(d->d_name) + len;
      if (sz > bufsz) {
        bufsz *= 2;
        if (sz > bufsz) bufsz = sz;
        buf = xrealloc(buf, bufsz);
      }
      d = (struct dirent *)buf;
#ifdef __MINGW32__
#if __MINGW32_MAJOR_VERSION < 3 || \
    (__MINGW32_MAJOR_VERSION == 3 && __MINGW32_MINOR_VERSION == 0)
      d->d_name = xmalloc(len);
#endif
#endif
      FAKE_DIR_ENTRY(d);
#ifdef _DIRENT_HAVE_D_NAMLEN
      d->d_namlen = len - 1;
#endif
      d->d_type = df->type;
      memcpy(d->d_name, df->name, len);
      return d;
    }
  }

  return 0;
}

/* On 64 bit ReliantUNIX (5.44 and above) in LFS mode, stat() is actually a
 * macro for stat64().  If stat is a macro, make a local wrapper function to
 * invoke it.
 *
 * On MS-Windows, stat() "succeeds" for foo/bar/. where foo/bar is a
 * regular file; fix that here.
 */
#if !defined(stat) && !defined(WINDOWS32) || defined(VMS)
#define local_stat stat
#else
static int local_stat(const char *path, struct stat *buf) {
  int e;
#ifdef WINDOWS32
  size_t plen = strlen(path);

  /* Make sure the parent of "." exists and is a directory, not a
     file.  This is because 'stat' on Windows normalizes the argument
     foo/. => foo without checking first that foo is a directory.  */
  if (plen > 1 && path[plen - 1] == '.' &&
      (path[plen - 2] == '/' || path[plen - 2] == '\\')) {
    char parent[MAXPATHLEN];

    strncpy(parent, path, plen - 2);
    parent[plen - 2] = '\0';
    if (stat(parent, buf) < 0 || !_S_ISDIR(buf->st_mode)) return -1;
  }
#endif

  EINTRLOOP(e, stat(path, buf));
  return e;
}
#endif

/* Similarly for lstat.  */
#if !defined(lstat) && !defined(WINDOWS32) || defined(VMS)
#define local_lstat lstat
#elif defined(WINDOWS32)
/* Windows doesn't support lstat().  */
#define local_lstat local_stat
#else
static int local_lstat(const char *path, struct stat *buf) {
  int e;
  EINTRLOOP(e, lstat(path, buf));
  return e;
}
#endif

void dir_setup_glob(glob_t *gl) {
  gl->gl_offs = 0;
  gl->gl_opendir = open_dirstream;
  gl->gl_readdir = read_dirstream;
  gl->gl_closedir = free;
  gl->gl_lstat = local_lstat;
  gl->gl_stat = local_stat;
}

void hash_init_directories(void) {
  hash_init(&directories, DIRECTORY_BUCKETS, directory_hash_1, directory_hash_2,
            directory_hash_cmp);
  hash_init(&directory_contents, DIRECTORY_BUCKETS, directory_contents_hash_1,
            directory_contents_hash_2, directory_contents_hash_cmp);
}
