Blob Blame History Raw
/* updatedb(8).

Copyright (C) 2005, 2007, 2008 Red Hat, Inc. All rights reserved.
This copyrighted material is made available to anyone wishing to use, modify,
copy, or redistribute it subject to the terms and conditions of the GNU General
Public License v.2.

This program 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, write to the Free Software Foundation, Inc., 51 Franklin
Street, Fifth Floor, Boston, MA 02110-1301, USA.

Author: Miloslav Trmac <mitr@redhat.com> */
#include <config.h>

#include <arpa/inet.h>
#include <assert.h>
#include <ctype.h>
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <grp.h>
#include <limits.h>
#include <locale.h>
#include <signal.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <unistd.h>

#include <mntent.h>
#include "error.h"
#include "fwriteerror.h"
#include "obstack.h"
#include "progname.h"
#include "stat-time.h"
#include "verify.h"
#include "xalloc.h"

#include "bind-mount.h"
#include "conf.h"
#include "db.h"
#include "lib.h"

#ifdef PROC_MOUNTS_PATH
#define MOUNT_TABLE_PATH PROC_MOUNTS_PATH
#else
#define MOUNT_TABLE_PATH _PATH_MOUNTED
#endif

/* A directory entry in memory */
struct entry
{
  size_t name_size;		/* Including the trailing NUL */
  bool is_directory;
  char name[];
};

/* Data for directory building */
struct dir_state
{
  /* Obstack of directory paths and 'struct entry' entries */
  struct obstack data_obstack;
  /* Obstack of 'void *' (struct entry *) lists */
  struct obstack list_obstack;
};

/* Time representation */
struct time
{
  uint64_t sec;
  uint32_t nsec;		/* 0 <= nsec < 1e9 */
};

/* A directory in memory, using storage in obstacks */
struct directory
{
  struct time time;
  void **entries;		/* Pointers to struct entry */
  size_t num_entries;
  char *path;			/* Absolute path */
};

 /* Time handling */

/* Get ctime from ST to TIME */
static void
time_get_ctime (struct time *t, const struct stat *st)
{
  t->sec = st->st_ctime;
  t->nsec = get_stat_ctime_ns (st);
  if (t->nsec >= 1000000000) /* Captive NTFS returns bogus values */
    t->nsec = 0;
  assert (t->nsec < 1000000000);
}

/* Get mtime from ST to TIME */
static void
time_get_mtime (struct time *t, const struct stat *st)
{
  t->sec = st->st_mtime;
  t->nsec = get_stat_mtime_ns (st);
  if (t->nsec >= 1000000000) /* Captive NTFS returns bogus values */
    t->nsec = 0;
  assert (t->nsec < 1000000000);
}

/* Compare times A and B */
static int
time_compare (const struct time *a, const struct time *b)
{
  if (a->sec < b->sec)
    return -1;
  if (a->sec > b->sec)
    return 1;
  if (a->nsec < b->nsec)
    return -1;
  if (a->nsec > b->nsec)
    return 1;
  return 0;
}

/* Is T recent enough that the filesystem could be changed without changing the
   timestamp again? */
static bool
time_is_current (const struct time *t)
{
  static struct time cache; /* = { 0, } */

  struct timeval tv;

  /* This is more difficult than it should be because Linux uses a cheaper time
     source for filesystem timestamps than for gettimeofday() and they can get
     slightly out of sync, see
     https://bugzilla.redhat.com/show_bug.cgi?id=244697 .  This affects even
     nanosecond timestamps (and don't forget that tv_nsec existence doesn't
     guarantee that the underlying filesystem has such resolution - it might be
     microseconds or even coarser).

     The worst case is probably FAT timestamps with 2-second resolution
     (although using such a filesystem violates POSIX file times requirements).

     So, to be on the safe side, require a >3.0 second difference (2 seconds to
     make sure the FAT timestamp changed, 1 more to account for the Linux
     timestamp races).  This large margin might make updatedb marginally more
     expensive, but it only makes a difference if the directory was very
     recently updated _and_ is will not be updated again until the next
     updatedb run; this is not likely to happen for most directories. */

  /* Cache gettimeofday () results to rule out obviously old time stamps;
     CACHE contains the earliest time we reject as too current. */
  if (time_compare (t, &cache) < 0)
    return false;
  gettimeofday (&tv, NULL);
  cache.sec = tv.tv_sec - 3;
  cache.nsec = tv.tv_usec * 1000;
  return time_compare (t, &cache) >= 0;
}

 /* Directory obstack handling */

/* Prepare STATE for reading directories */
static void
dir_state_init (struct dir_state *state)
{
  obstack_init (&state->data_obstack);
  obstack_init (&state->list_obstack);
}

/* Finish building a directory in STATE, store its data to DIR. */
static void
dir_finish (struct directory *dir, struct dir_state *state)
{
  dir->num_entries
    = OBSTACK_OBJECT_SIZE (&state->list_obstack) / sizeof (*dir->entries);
  dir->entries = obstack_finish (&state->list_obstack);
}

 /* Reading of the existing database */

/* The old database or old_db_is_closed.  old_db.fd == -1 if the database was
   never opened. */
static struct db old_db;
/* Header for unread directory from the old database or old_dir.path == NULL */
static struct directory old_dir; /* = { 0, }; */
/* true if old_db should not be accessed any more.  (old_db.fd cannot be closed
   immediatelly because that would release the lock on the database). */
static bool old_db_is_closed; /* = 0; */

/* Obstack for old_dir.path and old_dir_skip () */
static struct obstack old_dir_obstack;

/* Close old_db */
static void
old_db_close (void)
{
  old_dir.path = NULL;
  old_db_is_closed = true;
  /* The file will really be closed at the end of main (). */
}

/* Read next directory header, if any, to old_dir */
static void
old_dir_next_header (void)
{
  struct db_directory dir;

  if (old_db_is_closed)
    return;
  if (old_dir.path != NULL)
    obstack_free (&old_dir_obstack, old_dir.path);
  if (db_read (&old_db, &dir, sizeof (dir)) != 0)
    goto err;
  old_dir.time.sec = ntohll (dir.time_sec);
  old_dir.time.nsec = ntohl (dir.time_nsec);
  if (old_dir.time.nsec >= 1000000000)
    goto err;
  if (db_read_name (&old_db, &old_dir_obstack) != 0)
    goto err;
  obstack_1grow (&old_dir_obstack, 0);
  old_dir.path = obstack_finish (&old_dir_obstack);
  return;

 err:
  old_db_close ();
}

/* Skip next directory in old_db */
static void
old_dir_skip (void)
{
  void *mark;

  mark = obstack_alloc (&old_dir_obstack, 0);
  for (;;)
    {
      struct db_entry entry;
      void *p;

      if (db_read (&old_db, &entry, sizeof (entry)) != 0)
	goto err;
      switch (entry.type)
	{
	case DBE_NORMAL: case DBE_DIRECTORY:
	  break;

	case DBE_END:
	  goto done;

	default:
	  goto err;
	}
      if (db_read_name (&old_db, &old_dir_obstack) != 0)
	goto err;
      p = obstack_finish (&old_dir_obstack);
      obstack_free (&old_dir_obstack, p);
    }
 done:
  return;

 err:
  (void)obstack_finish (&old_dir_obstack);
  obstack_free (&old_dir_obstack, mark);
  old_db_close ();
}

/* Open the old database and prepare for reading it.  Return a file descriptor
   for the database (even if its contents are not valid), -1 on error opening
   the file. */
static int
old_db_open (void)
{
  struct obstack obstack;
  int fd;
  struct db_header hdr;
  const char *src;
  uint32_t size;

  /* Use O_RDWR, not O_RDONLY, to be able to lock the file. */
  fd = open (conf_output, O_RDWR);
  if (fd == -1)
    {
      old_db.fd = -1;
      goto err;
    }
  if (db_open (&old_db, &hdr, fd, conf_output, true) != 0)
    {
      old_db.fd = -1;
      goto err;
    }
  size = ntohl (hdr.conf_size);
  if (size != conf_block_size)
    goto err_old_db;
  obstack_init (&obstack);
  obstack_alignment_mask (&obstack) = 0;
  if (db_read_name (&old_db, &obstack) != 0)
    goto err_obstack;
  obstack_1grow (&obstack, 0);
  if (strcmp (obstack_finish (&obstack), conf_scan_root) != 0)
    goto err_obstack;
  obstack_free (&obstack, NULL);
  src = conf_block;
  while (size != 0)
    {
      char buf[BUFSIZ];
      size_t run;

      run = sizeof (buf);
      if (run > size)
	run = size;
      if (db_read (&old_db, buf, run) != 0)
	goto err_old_db;
      if (memcmp (src, buf, run) != 0)
	goto err_old_db;
      src += run;
      size -= run;
    }
  obstack_init (&old_dir_obstack);
  obstack_alignment_mask (&old_dir_obstack) = 0;
  old_dir_next_header ();
  return fd;

 err_obstack:
  obstack_free (&obstack, NULL);
 err_old_db:
  old_db_close ();
 err:
  old_db_is_closed = true;
  return fd;
}

 /* $PRUNEFS handling */

static int
cmp_string_pointer (const void *xa, const void *xb)
{
  const char *a;
  char *const *b;

  a = xa;
  b = xb;
  return strcmp (a, *b);
}

/* Return true if PATH is a mount point of an excluded filesystem */
static bool
filesystem_is_excluded (const char *path)
{
  static char *type; /* = NULL; */
  static size_t type_size; /* = 0; */

  FILE *f;
  struct mntent *me;
  bool res;

  if (conf_debug_pruning != false)
    /* This is debuging output, don't mark anything for translation */
    fprintf (stderr, "Checking whether filesystem `%s' is excluded:\n", path);
  res = false;
  f = setmntent (MOUNT_TABLE_PATH, "r");
  if (f == NULL)
    goto err;
  while ((me = getmntent (f)) != NULL)
    {
      char *p;
      size_t size;

      if (conf_debug_pruning != false)
	/* This is debuging output, don't mark anything for translation */
	fprintf (stderr, " `%s', type `%s'\n", me->mnt_dir, me->mnt_type);
      size = strlen (me->mnt_type) + 1;
      while (size > type_size)
	type = x2realloc (type, &type_size);
      memcpy (type, me->mnt_type, size);
      for (p = type; *p != 0; p++)
	*p = toupper((unsigned char)*p);
      if (bsearch (type, conf_prunefs.entries, conf_prunefs.len,
		   sizeof (*conf_prunefs.entries), cmp_string_pointer) != NULL)
	{
	  char *dir;

#ifndef PROC_MOUNTS_PATH
	  dir = canonicalize_file_name (me->mnt_dir);
	  if (dir == NULL)
	    dir = me->mnt_dir;
#else
	  /* Paths in /proc/self/mounts contain no symbolic links.  Besides
	     avoiding a few system calls, avoiding the realpath () avoids hangs
	     if the filesystem is unavailable hard-mounted NFS. */
	  dir = me->mnt_dir;
#endif
	  if (conf_debug_pruning != false)
	    /* This is debuging output, don't mark anything for translation */
	    fprintf (stderr, " => type matches, dir `%s'\n", dir);
	  if (strcmp (path, dir) == 0)
	    res = true;
	  if (dir != me->mnt_dir)
	    free(dir);
	  if (res != false)
	    goto err_f;
	}
    }
 err_f:
  endmntent (f);
 err:
  if (conf_debug_pruning != false)
    /* This is debuging output, don't mark anything for translation */
    fprintf (stderr, "...done\n");
  return res;
}

 /* Filesystem scanning */

/* The new database */
static FILE *new_db;
/* A _temporary_ file name, or NULL if there is no temporary file */
static char *new_db_filename;

/* Global obstacks for filesystem scanning */
static struct dir_state scan_dir_state;

/* Next conf_prunepaths entry */
static size_t conf_prunepaths_index; /* = 0; */

/* Forward declaration */
static int scan (char *path, int *cwd_fd, const struct stat *st_parent,
		 const char *relative);

/* Write DIR to new_db. */
static void
write_directory (const struct directory *dir)
{
  struct db_directory header;
  struct db_entry entry;
  size_t i;

  memset (&header, 0, sizeof (header));
  header.time_sec = htonll (dir->time.sec);
  assert (dir->time.nsec < 1000000000);
  header.time_nsec = htonl (dir->time.nsec);
  fwrite (&header, sizeof (header), 1, new_db);
  fwrite (dir->path, 1, strlen (dir->path) + 1, new_db);
  for (i = 0; i < dir->num_entries; i++)
    {
      struct entry *e;

      e = dir->entries[i];
      entry.type = e->is_directory != false ? DBE_DIRECTORY : DBE_NORMAL;
      fwrite (&entry, sizeof (entry), 1, new_db);
      fwrite (e->name, 1, e->name_size, new_db);
    }
  entry.type = DBE_END;
  fwrite (&entry, sizeof (entry), 1, new_db);
}

/* Scan subdirectories of the current working directory, which has ST, among
   entries in DIR, and write results to new_db.  The current working directory
   is not guaranteed to be preserved on return from this function. */
static void
scan_subdirs (const struct directory *dir, const struct stat *st)
{
  char *path;
  int cwd_fd;
  size_t path_size, prefix_len, i;

  prefix_len = strlen (dir->path);
  path_size = prefix_len + 1;
  path = xmalloc (path_size);
  memcpy (path, dir->path, prefix_len);
  assert (prefix_len != 0);
  if (dir->path[prefix_len - 1] != '/') /* "/" => "/bin", not "//bin" */
    {
      path[prefix_len] = '/';
      prefix_len++;
    }
  cwd_fd = -1;
  for (i = 0; i < dir->num_entries; i++)
    {
      struct entry *e;

      e = dir->entries[i];
      if (e->is_directory != false)
	{
	  /* Verified in copy_old_dir () and scan_cwd () */
	  while (prefix_len + e->name_size > path_size)
	    path = x2realloc (path, &path_size);
	  memcpy (path + prefix_len, e->name, e->name_size);
	  if (scan (path, &cwd_fd, st, e->name) != 0)
	    goto err_cwd_fd;
	}
    }
 err_cwd_fd:
  if (cwd_fd != -1)
    close (cwd_fd);
  free (path);
}

/* If *CWD_FD != -1, open ".", store fd to *CWD_FD; then chdir (RELATIVE),
   failing if there is a race condition; RELATIVE is supposed to match OLD_ST.
   Return 0 if OK, -1 on error */
static int
safe_chdir (int *cwd_fd, const char *relative, const struct stat *old_st)
{
  struct stat st;

  if (*cwd_fd == -1)
    {
      int fd;
      
      fd = open (".", O_RDONLY);
      if (fd == -1)
	return -1;
      *cwd_fd = fd;
    }
  if (chdir (relative) != 0 || lstat (".", &st) != 0)
    return -1;
  if (old_st->st_dev != st.st_dev || old_st->st_ino != st.st_ino)
    return -1; /* Race condition, skip the subtree */
  return 0;
}

/* Read directory after old_dir to DEST in scan_dir_state;
   Return -1 on error, 1 if DEST contains a subdirectory, 0 otherwise. */
static int
copy_old_dir (struct directory *dest)
{
  bool have_subdir;
  size_t i;
  void *mark, *p;

  if (old_db_is_closed || old_dir.path == NULL)
    goto err;
  mark = obstack_alloc (&scan_dir_state.data_obstack, 0);
  have_subdir = false;
  for (;;)
    {
      struct db_entry entry;
      struct entry *e;
      bool is_directory;
      size_t size;

      if (db_read (&old_db, &entry, sizeof (entry)) != 0)
	goto err_obstack;
      switch (entry.type)
	{
	case DBE_NORMAL:
	  is_directory = false;
	  break;

	case DBE_DIRECTORY:
	  is_directory = true;
	  break;

	case DBE_END:
	  goto done;

	default:
	  goto err_obstack;
	}
      {
	verify (offsetof (struct entry, name) <= OBSTACK_SIZE_MAX);
      }
      obstack_blank (&scan_dir_state.data_obstack,
		     offsetof (struct entry, name));
      if (db_read_name (&old_db, &scan_dir_state.data_obstack) != 0)
	goto err_obstack;
      obstack_1grow (&scan_dir_state.data_obstack, 0);
      size = (OBSTACK_OBJECT_SIZE (&scan_dir_state.data_obstack)
	      - offsetof (struct entry, name));
      if (size > OBSTACK_SIZE_MAX)
	{
	  error (0, 0, _("file name length %zu is too large"), size);
	  goto err_obstack;
	}
      e = obstack_finish (&scan_dir_state.data_obstack);
      e->name_size = size;
      e->is_directory = is_directory;
      if (is_directory != false)
	have_subdir = true;
      obstack_ptr_grow (&scan_dir_state.list_obstack, e);
      if (conf_verbose != false)
	printf ("%s/%s\n", dest->path, e->name);
    }
 done:
  dir_finish (dest, &scan_dir_state);
  for (i = 0; i + 1 < dest->num_entries; i++)
    {
      struct entry *a, *b;

      a = dest->entries[i];
      b = dest->entries[i + 1];
      if (strcmp (a->name, b->name) >= 0)
	goto err_obstack;
    }
  return have_subdir;

 err_obstack:
  (void)obstack_finish (&scan_dir_state.data_obstack);
  obstack_free (&scan_dir_state.data_obstack, mark);
  p = obstack_finish (&scan_dir_state.list_obstack);
  obstack_free (&scan_dir_state.list_obstack, p);
 err:
  old_db_close ();
  return -1;
}

/* Compare two "void *" (struct entry *) values */
static int
cmp_entries (const void *xa, const void *xb)
{
  void *const *a_, *const *b_;
  struct entry *a, *b;

  a_ = xa;
  b_ = xb;
  a = *a_;
  b = *b_;
  return strcmp (a->name, b->name);
}

static DIR *
opendir_noatime (const char *path)
{
#if defined (HAVE_FDOPENDIR) && defined (O_DIRECTORY) && defined (O_NOATIME)
  static bool noatime_failed; /* = false; */

  if (noatime_failed == false)
    {
      int fd;

      fd = open (path, O_RDONLY | O_DIRECTORY | O_NOATIME);
      if (fd != -1)
	return fdopendir (fd);
      /* EPERM is fairly O_NOATIME-specific; missing access rights cause
	 EACCES. */
      else if (errno != EPERM)
	return NULL;
      noatime_failed = true;
    }
#endif
  return opendir (path);
}

/* Scan current working directory (DEST.path) to DEST in scan_dir_state;
   Return -1 if "." can't be opened, 1 if DEST contains a subdirectory,
   0 otherwise. */
static int
scan_cwd (struct directory *dest)
{
  DIR *dir;
  struct dirent *de;
  bool have_subdir;

  dir = opendir_noatime (".");
  if (dir == NULL)
    return -1;
  have_subdir = false;
  while ((de = readdir (dir)) != NULL)
    {
      struct entry *e;
      size_t name_size, entry_size;

      if (strcmp (de->d_name, ".") == 0 || strcmp (de->d_name, "..") == 0)
	continue;
      name_size = strlen (de->d_name) + 1;
      if (name_size == 1)
	{
	  /* Unfortunately, this does happen, and mere assert() does not give
	     users enough information to complain to the right people. */
	  error (0, 0,
		 _("file system error: zero-length file name in directory %s"),
		 dest->path);
	  continue;
	}
      assert (name_size > 1);
      entry_size = offsetof (struct entry, name) + name_size;
      if (entry_size > OBSTACK_SIZE_MAX)
	{
	  error (0, 0, _("file name length %zu is too large"), name_size);
	  continue;
	}
      e = obstack_alloc (&scan_dir_state.data_obstack, entry_size);
      e->name_size = name_size;
      memcpy (e->name, de->d_name, name_size);
      e->is_directory = false;
      /* The check for DT_DIR is to handle platforms which have d_type, but
	 require a feature macro to define DT_* */
#if defined (HAVE_STRUCT_DIRENT_D_TYPE) && defined (DT_DIR)
      if (de->d_type == DT_DIR)
	e->is_directory = true;
      else if (de->d_type == DT_UNKNOWN)
#endif
	{
	  struct stat st;
	  
	  if (lstat (e->name, &st) == 0 && S_ISDIR (st.st_mode))
	    e->is_directory = true;
	}
      if (e->is_directory != false)
	have_subdir = true;
      obstack_ptr_grow (&scan_dir_state.list_obstack, e);
      if (conf_verbose != false)
	printf ("%s/%s\n", dest->path, e->name);
    }
  closedir (dir);
  dir_finish (dest, &scan_dir_state);
  qsort (dest->entries, dest->num_entries, sizeof (*dest->entries),
	 cmp_entries);
  return have_subdir;
}

/* Scan filesystem subtree rooted at PATH, which is "./RELATIVE", and write
   results to new_db.  Try to preserve current working directory (opening a
   file descriptor to it in *CWD_FD, if *CWD_FD == -1).  Use ST_PARENT for
   checking whether a PATH is a mount point.  Return -1 if the current working
   directory couldn't be preserved, 0 otherwise.

   Note that PATH may be longer than PATH_MAX, so relative file names should
   always be used. */
static int
scan (char *path, int *cwd_fd, const struct stat *st_parent,
      const char *relative)
{
  struct directory dir;
  struct stat st;
  struct time mtime;
  void *entries_mark;
  int cmp, res;
  bool have_subdir, did_chdir;

  if (string_list_contains_dir_path (&conf_prunepaths, &conf_prunepaths_index,
				     path))
    {
      if (conf_debug_pruning != false)
	/* This is debuging output, don't mark anything for translation */
	fprintf (stderr, "Skipping `%s': in prunepaths\n", path);
      goto err;
    }
  if (conf_prune_bind_mounts != false && is_bind_mount (path))
    {
      if (conf_debug_pruning != false)
	/* This is debuging output, don't mark anything for translation */
	fprintf (stderr, "Skipping `%s': bind mount\n", path);
      goto err;
    }
  if (bsearch (relative, conf_prunenames.entries, conf_prunenames.len,
	       sizeof (*conf_prunenames.entries), cmp_string_pointer) != NULL)
    {
      if (conf_debug_pruning != false)
	/* This is debuging output, don't mark anything for translation */
	fprintf (stderr, "Skipping `%s': in prunenames\n", path);
      goto err;
    }
  if (lstat (relative, &st) != 0)
    goto err;
  if (st.st_dev != st_parent->st_dev && filesystem_is_excluded (path))
    {
      if (conf_debug_pruning != false)
	/* This is debuging output, don't mark anything for translation */
	fprintf (stderr, "Skipping `%s': in prunefs\n", path);
      goto err;
    }
  /* "relative" may now become a symlink to somewhere else.  So we use it only
     in safe_chdir (). */
  entries_mark = obstack_alloc (&scan_dir_state.data_obstack, 0);
  dir.path = path;
  time_get_ctime (&dir.time, &st);
  time_get_mtime (&mtime, &st);
  if (time_compare (&dir.time, &mtime) < 0)
    dir.time = mtime;
  while (old_dir.path != NULL && (cmp = dir_path_cmp (old_dir.path, path)) < 0)
    {
      old_dir_skip ();
      old_dir_next_header ();
    }
  did_chdir = false;
  have_subdir = false;
  if (old_dir.path != NULL && cmp == 0
      && time_compare (&dir.time, &old_dir.time) == 0
      && (dir.time.sec != 0 || dir.time.nsec != 0))
    {
      res = copy_old_dir (&dir);
      if (res != -1)
	{
	  have_subdir = res;
	  old_dir_next_header ();
	  goto have_dir;
	}
    }
  if (time_is_current (&dir.time))
    {
      /* The directory might be changing right now and we can't be sure the
	 timestamp will be changed again if more changes happen very soon, mark
	 the timestamp as invalid to force rescanning the directory next time
	 updatedb is run. */
      dir.time.sec = 0;
      dir.time.nsec = 0;
    }
  did_chdir = true;
  if (safe_chdir (cwd_fd, relative, &st) != 0)
    goto err_chdir;
  res = scan_cwd (&dir);
  if (res == -1)
    goto err_chdir;
  have_subdir = res;
 have_dir:
  write_directory (&dir);
  if (have_subdir != false)
    {
      if (did_chdir == false)
	{
	  did_chdir = true;
	  if (safe_chdir (cwd_fd, relative, &st) != 0)
	    goto err_entries;
	}
      scan_subdirs (&dir, &st);
    }
 err_entries:
  obstack_free (&scan_dir_state.list_obstack, dir.entries);
  obstack_free (&scan_dir_state.data_obstack, entries_mark);
 err_chdir:
  if (did_chdir != false && *cwd_fd != -1 && fchdir (*cwd_fd) != 0)
    return -1;
 err:
  return 0;
}

 /* Unlinking of temporary database file */

/* An absolute path to the file to unlink or NULL */
static const char *unlink_path; /* = NULL; */

/* Signals which try to unlink unlink_path */
static sigset_t unlink_sigset;

/* Unlink unlink_path */
static void
unlink_db (void)
{
  if (unlink_path != NULL)
    unlink (unlink_path);
}

/* SIGINT/SIGTERM handler */
static void attribute__ ((noreturn))
unlink_signal (int sig)
{
  sigset_t mask;

  unlink_db ();
  signal (sig, SIG_DFL);
  sigemptyset (&mask);
  sigaddset (&mask, sig);
  sigprocmask (SIG_UNBLOCK, &mask, NULL);
  raise (sig);
  _exit (EXIT_FAILURE);
}

/* Set unlink_path to PATH (which must remain valid until next unlink_set () */
static void
unlink_set (const char *path)
{
  sigset_t old;

  sigprocmask (SIG_BLOCK, &unlink_sigset, &old);
  unlink_path = path;
  sigprocmask (SIG_SETMASK, &old, NULL);
}

/* Initialize the unlinking code */
static void
unlink_init (void)
{
  static const int signals[] = { SIGABRT, SIGINT, SIGTERM };

  struct sigaction sa;
  size_t i;

  atexit (unlink_db);
  sigemptyset (&unlink_sigset);
  for (i = 0; i < ARRAY_SIZE(signals); i++)
    sigaddset (&unlink_sigset, signals[i]);
  sa.sa_handler = unlink_signal;
  sa.sa_mask = unlink_sigset;
  sa.sa_flags = 0;
  for (i = 0; i < ARRAY_SIZE(signals); i++)
    sigaction (signals[i], &sa, NULL);
}

 /* Top level */

/* Open a temporary file for the new database and initialize its header
   and configuration block.  Exit on error. */
static void
new_db_open (void)
{
  static const uint8_t magic[] = DB_MAGIC;

  struct db_header db_header;
  char *filename;
  int db_fd;
  
  filename = xmalloc (strlen (conf_output) + 8);
  sprintf (filename, "%s.XXXXXX", conf_output);
  db_fd = mkstemp (filename);
  if (db_fd == -1)
    error (EXIT_FAILURE, 0, _("can not open a temporary file for `%s'"),
	   conf_output);
  new_db_filename = filename;
  unlink_set (filename);
  new_db = fdopen (db_fd, "wb");
  if (new_db == NULL)
    error (EXIT_FAILURE, errno, _("can not open `%s'"), new_db_filename);
  memset (&db_header, 0, sizeof (db_header));
  {
    verify (sizeof (db_header.magic) == sizeof (magic));
  }
  memcpy (db_header.magic, &magic, sizeof (magic));
  if (conf_block_size > UINT32_MAX)
    error (EXIT_FAILURE, 0, _("configuration is too large"));
  db_header.conf_size = htonl (conf_block_size);
  db_header.version = DB_VERSION_0;
  db_header.check_visibility = conf_check_visibility;
  fwrite (&db_header, sizeof (db_header), 1, new_db);
  fwrite (conf_scan_root, 1, strlen (conf_scan_root) + 1, new_db);
  fwrite (conf_block, 1, conf_block_size, new_db);
}

/* Set up permissions of new_db_filename.  Exit on error. */
static void
new_db_setup_permissions (void)
{
  mode_t mode;

  if (conf_check_visibility != false)
    {
      struct group *grp;

      grp = getgrnam (GROUPNAME);
      if (grp == NULL)
	error (EXIT_FAILURE, errno, _("can not find group `%s'"), GROUPNAME);
      if (chown (new_db_filename, (uid_t)-1, grp->gr_gid) != 0)
	error (EXIT_FAILURE, errno,
	       _("can not change group of file `%s' to `%s'"), new_db_filename,
	       GROUPNAME);
      mode = S_IRUSR | S_IWUSR | S_IRGRP;
    }
  else /* Permissions as if open (..., O_CREAT | O_WRONLY, 0666) */
    {
      mode_t mask;

      mask = umask (S_IRWXU | S_IRWXG | S_IRWXG);
      umask (mask);
      mode = ((S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH)
	      & ~mask);
    }
  if (chmod (new_db_filename, mode) != 0)
    error (EXIT_FAILURE, errno, _("can not change permissions of file `%s'"),
	   new_db_filename);
}

int
main (int argc, char *argv[])
{
  struct stat st;
  int lock_file_fd, cwd_fd;

  set_program_name (argv[0]);
  dir_path_cmp_init ();
  setlocale (LC_ALL, "");
  bindtextdomain (PACKAGE_NAME, LOCALEDIR);
  textdomain (PACKAGE_NAME);
  conf_prepare (argc, argv);
  if (conf_prune_bind_mounts != false)
    bind_mount_init (MOUNTINFO_PATH);
  lock_file_fd = old_db_open ();
  if (lock_file_fd != -1)
    {
      struct flock lk;

      lk.l_type = F_WRLCK;
      lk.l_whence = SEEK_SET;
      lk.l_start = 0;
      lk.l_len = 0;
      if (fcntl (lock_file_fd, F_SETLK, &lk) == -1)
	{
	  if (errno == EACCES || errno == EAGAIN)
	    error (EXIT_FAILURE, 0,
		   _("`%s' is locked (probably by an earlier updatedb)"),
		   conf_output);
	  error (EXIT_FAILURE, errno, _("can not lock `%s'"), conf_output);
	}
    }
  unlink_init ();
  new_db_open ();
  dir_state_init (&scan_dir_state);
  if (chdir (conf_scan_root) != 0)
    error (EXIT_FAILURE, errno, _("can not change directory to `%s'"),
	   conf_scan_root);
  if (lstat (".", &st) != 0)
    error (EXIT_FAILURE, errno, _("can not stat () `%s'"), conf_scan_root);
  cwd_fd = -1;
  scan (conf_scan_root, &cwd_fd, &st, ".");
  if (cwd_fd != -1)
    close (cwd_fd);
  if (fwriteerror (new_db))
    error (EXIT_FAILURE, errno, _("I/O error while writing to `%s'"),
	   new_db_filename);
  new_db_setup_permissions ();
  if (rename (new_db_filename, conf_output) != 0)
    error (EXIT_FAILURE, errno, _("error replacing `%s'"), conf_output);
  /* There is really no race condition in removing other files now: unlink ()
     only removes the directory entry (not symlink targets), and the file had
     to be intentionally placed there to match the mkstemp () result.  So any
     attacker can at most remove their own data. */
  unlink_set (NULL);
  free (new_db_filename);
  if (old_db.fd != -1)
    db_close(&old_db); /* Releases the lock */
  else if (lock_file_fd != -1)
    /* old_db is invalid, but the file was used for locking */
    close(lock_file_fd); /* Releases the lock */
  if (fwriteerror (stdout))
    error (EXIT_FAILURE, errno,
	   _("I/O error while writing to standard output"));
  return EXIT_SUCCESS;
}