/* GNU dump extensions to tar.

   Copyright (C) 1988, 1992, 1993, 1994, 1996, 1997, 1999, 2000, 2001,
   2003, 2004 Free Software Foundation, Inc.

   This program 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 2, or (at your option) any later
   version.

   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.,
   59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.  */

#include <system.h>
#include <getline.h>
#include <hash.h>
#include <quotearg.h>
#include "common.h"
#define obstack_chunk_alloc xmalloc
#define obstack_chunk_free free
#include <obstack.h>

/* Incremental dump specialities.  */

/* Which child files to save under a directory.  */
enum children {NO_CHILDREN, CHANGED_CHILDREN, ALL_CHILDREN};

/* Directory attributes.  */
struct directory
  {
    dev_t device_number;	/* device number for directory */
    ino_t inode_number;		/* inode number for directory */
    enum children children;
    bool nfs;
    bool found;
    char name[1];		/* file name of directory */
  };

static Hash_table *directory_table;

#if HAVE_ST_FSTYPE_STRING
  static char const nfs_string[] = "nfs";
# define NFS_FILE_STAT(st) (strcmp ((st).st_fstype, nfs_string) == 0)
#else
# define ST_DEV_MSB(st) (~ (dev_t) 0 << (sizeof (st).st_dev * CHAR_BIT - 1))
# define NFS_FILE_STAT(st) (((st).st_dev & ST_DEV_MSB (st)) != 0)
#endif

/* Calculate the hash of a directory.  */
static unsigned
hash_directory (void const *entry, unsigned n_buckets)
{
  struct directory const *directory = entry;
  return hash_string (directory->name, n_buckets);
}

/* Compare two directories for equality.  */
static bool
compare_directories (void const *entry1, void const *entry2)
{
  struct directory const *directory1 = entry1;
  struct directory const *directory2 = entry2;
  return strcmp (directory1->name, directory2->name) == 0;
}

/* Create and link a new directory entry for directory NAME, having a
   device number DEV and an inode number INO, with NFS indicating
   whether it is an NFS device and FOUND indicating whether we have
   found that the directory exists.  */
static struct directory *
note_directory (char const *name, dev_t dev, ino_t ino, bool nfs, bool found)
{
  size_t size = offsetof (struct directory, name) + strlen (name) + 1;
  struct directory *directory = xmalloc (size);

  directory->device_number = dev;
  directory->inode_number = ino;
  directory->children = CHANGED_CHILDREN;
  directory->nfs = nfs;
  directory->found = found;
  strcpy (directory->name, name);

  if (! ((directory_table
	  || (directory_table = hash_initialize (0, 0, hash_directory,
						 compare_directories, 0)))
	 && hash_insert (directory_table, directory)))
    xalloc_die ();

  return directory;
}

/* Return a directory entry for a given file NAME, or zero if none found.  */
static struct directory *
find_directory (char *name)
{
  if (! directory_table)
    return 0;
  else
    {
      size_t size = offsetof (struct directory, name) + strlen (name) + 1;
      struct directory *dir = alloca (size);
      strcpy (dir->name, name);
      return hash_lookup (directory_table, dir);
    }
}

static int
compare_dirents (const void *first, const void *second)
{
  return strcmp ((*(char *const *) first) + 1,
		 (*(char *const *) second) + 1);
}

/* Recursively scan the given directory. */
static void
scan_directory (struct obstack *stk, char *dir_name, dev_t device)
{
  char *dirp = savedir (dir_name);	/* for scanning directory */
  char const *entry;	/* directory entry being scanned */
  size_t entrylen;	/* length of directory entry */
  char *name_buffer;		/* directory, `/', and directory member */
  size_t name_buffer_size;	/* allocated size of name_buffer, minus 2 */
  size_t name_length;		/* used length in name_buffer */
  struct directory *directory;  /* for checking if already seen */
  enum children children;

  if (! dirp)
    {
      savedir_error (dir_name);
    }
  errno = 0;

  name_buffer_size = strlen (dir_name) + NAME_FIELD_SIZE;
  name_buffer = xmalloc (name_buffer_size + 2);
  strcpy (name_buffer, dir_name);
  if (! ISSLASH (dir_name[strlen (dir_name) - 1]))
    strcat (name_buffer, "/");
  name_length = strlen (name_buffer);

  directory = find_directory (dir_name);
  children = directory ? directory->children : CHANGED_CHILDREN;

  if (dirp && children != NO_CHILDREN)
    for (entry = dirp;
	 (entrylen = strlen (entry)) != 0;
	 entry += entrylen + 1)
      {
	if (name_buffer_size <= entrylen + name_length)
	  {
	    do
	      name_buffer_size += NAME_FIELD_SIZE;
	    while (name_buffer_size <= entrylen + name_length);
	    name_buffer = xrealloc (name_buffer, name_buffer_size + 2);
	  }
	strcpy (name_buffer + name_length, entry);

	if (excluded_name (name_buffer))
	  obstack_1grow (stk, 'N');
	else
	  {
	    struct stat stat_data;

	    if (deref_stat (dereference_option, name_buffer, &stat_data))
	      {
		stat_diag (name_buffer);
		continue;
	      }

	    if (S_ISDIR (stat_data.st_mode))
	      {
		bool nfs = NFS_FILE_STAT (stat_data);

		if ((directory = find_directory (name_buffer)) != NULL)
		  {
		    /* With NFS, the same file can have two different devices
		       if an NFS directory is mounted in multiple locations,
		       which is relatively common when automounting.
		       To avoid spurious incremental redumping of
		       directories, consider all NFS devices as equal,
		       relying on the i-node to establish differences.  */

		    if (! (((directory->nfs & nfs)
			    || directory->device_number == stat_data.st_dev)
			   && directory->inode_number == stat_data.st_ino))
			{
			  if (verbose_option)
			    WARN ((0, 0, _("%s: Directory has been renamed"),
				   quotearg_colon (name_buffer)));
			  directory->children = ALL_CHILDREN;
			  directory->nfs = nfs;
			  directory->device_number = stat_data.st_dev;
			  directory->inode_number = stat_data.st_ino;
			}
		    directory->found = 1;
		  }
		else
		  {
		    if (verbose_option)
		      WARN ((0, 0, _("%s: Directory is new"),
			     quotearg_colon (name_buffer)));
		    directory = note_directory (name_buffer,
						stat_data.st_dev,
						stat_data.st_ino, nfs, 1);
		    directory->children =
		      ((listed_incremental_option
			|| OLDER_STAT_TIME (stat_data, m)
			|| (after_date_option
			    && OLDER_STAT_TIME (stat_data, c)))
		       ? ALL_CHILDREN
		       : CHANGED_CHILDREN);
		  }

		if (one_file_system_option && device != stat_data.st_dev)
		  directory->children = NO_CHILDREN;
		else if (children == ALL_CHILDREN)
		  directory->children = ALL_CHILDREN;

		obstack_1grow (stk, 'D');
	      }

	    else if (one_file_system_option && device != stat_data.st_dev)
	      obstack_1grow (stk, 'N');

#ifdef S_ISHIDDEN
	    else if (S_ISHIDDEN (stat_data.st_mode))
	      {
		obstack_1grow (stk, 'D');
		obstack_grow (stk, entry, entrylen);
		obstack_grow (stk, "A", 2);
		continue;
	      }
#endif

	    else
	      if (children == CHANGED_CHILDREN
		  && OLDER_STAT_TIME (stat_data, m)
		  && (!after_date_option || OLDER_STAT_TIME (stat_data, c)))
		obstack_1grow (stk, 'N');
	      else
		obstack_1grow (stk, 'Y');
	  }

	obstack_grow (stk, entry, entrylen + 1);
      }

  obstack_grow (stk, "\000\000", 2);

  free (name_buffer);
  if (dirp)
    free (dirp);
}

/* Sort the contents of the obstack, and convert it to the char * */
static char *
sort_obstack (struct obstack *stk)
{
  char *pointer = obstack_finish (stk);
  size_t counter;
  char *cursor;
  char *buffer;
  char **array;
  char **array_cursor;

  counter = 0;
  for (cursor = pointer; *cursor; cursor += strlen (cursor) + 1)
    counter++;

  if (!counter)
    return NULL;

  array = obstack_alloc (stk, sizeof (char *) * (counter + 1));

  array_cursor = array;
  for (cursor = pointer; *cursor; cursor += strlen (cursor) + 1)
    *array_cursor++ = cursor;
  *array_cursor = 0;

  qsort (array, counter, sizeof (char *), compare_dirents);

  buffer = xmalloc (cursor - pointer + 2);

  cursor = buffer;
  for (array_cursor = array; *array_cursor; array_cursor++)
    {
      char *string = *array_cursor;

      while ((*cursor++ = *string++))
	continue;
    }
  *cursor = '\0';
  return buffer;
}

char *
get_directory_contents (char *dir_name, dev_t device)
{
  struct obstack stk;
  char *buffer;

  obstack_init (&stk);
  scan_directory (&stk, dir_name, device);
  buffer = sort_obstack (&stk);
  obstack_free (&stk, NULL);
  return buffer;
}



static FILE *listed_incremental_stream;

void
read_directory_file (void)
{
  int fd;
  FILE *fp;
  char *buf = 0;
  size_t bufsize;

  /* Open the file for both read and write.  That way, we can write
     it later without having to reopen it, and don't have to worry if
     we chdir in the meantime.  */
  fd = open (listed_incremental_option, O_RDWR | O_CREAT, MODE_RW);
  if (fd < 0)
    {
      open_error (listed_incremental_option);
      return;
    }

  fp = fdopen (fd, "r+");
  if (! fp)
    {
      open_error (listed_incremental_option);
      close (fd);
      return;
    }

  listed_incremental_stream = fp;

  if (0 < getline (&buf, &bufsize, fp))
    {
      char *ebuf;
      int n;
      long lineno = 1;
      unsigned long u = (errno = 0, strtoul (buf, &ebuf, 10));
      time_t t = u;
      if (buf == ebuf || (u == 0 && errno == EINVAL))
	ERROR ((0, 0, "%s:1: %s", quotearg_colon (listed_incremental_option),
		_("Invalid time stamp")));
      else if (t != u || (u == -1 && errno == ERANGE))
	ERROR ((0, 0, "%s:1: %s", quotearg_colon (listed_incremental_option),
		_("Time stamp out of range")));
      else
	{
	  newer_mtime_option.tv_sec = t;
	  newer_mtime_option.tv_nsec = 0;
	}

      while (0 < (n = getline (&buf, &bufsize, fp)))
	{
	  dev_t dev;
	  ino_t ino;
	  bool nfs = buf[0] == '+';
	  char *strp = buf + nfs;

	  lineno++;

	  if (buf[n - 1] == '\n')
	    buf[n - 1] = '\0';

	  errno = 0;
	  dev = u = strtoul (strp, &ebuf, 10);
	  if (strp == ebuf || (u == 0 && errno == EINVAL))
	    ERROR ((0, 0, "%s:%ld: %s",
		    quotearg_colon (listed_incremental_option), lineno,
		    _("Invalid device number")));
	  else if (dev != u || (u == -1 && errno == ERANGE))
	    ERROR ((0, 0, "%s:%ld: %s",
		    quotearg_colon (listed_incremental_option), lineno,
		    _("Device number out of range")));
	  strp = ebuf;

	  errno = 0;
	  ino = u = strtoul (strp, &ebuf, 10);
	  if (strp == ebuf || (u == 0 && errno == EINVAL))
	    ERROR ((0, 0, "%s:%ld: %s",
		    quotearg_colon (listed_incremental_option), lineno,
		    _("Invalid inode number")));
	  else if (ino != u || (u == -1 && errno == ERANGE))
	    ERROR ((0, 0, "%s:%ld: %s",
		    quotearg_colon (listed_incremental_option), lineno,
		    _("Inode number out of range")));
	  strp = ebuf;

	  strp++;
	  unquote_string (strp);
	  note_directory (strp, dev, ino, nfs, 0);
	}
    }

  if (ferror (fp))
    read_error (listed_incremental_option);
  if (buf)
    free (buf);
}

/* Output incremental data for the directory ENTRY to the file DATA.
   Return nonzero if successful, preserving errno on write failure.  */
static bool
write_directory_file_entry (void *entry, void *data)
{
  struct directory const *directory = entry;
  FILE *fp = data;

  if (directory->found)
    {
      int e;
      char *str = quote_copy_string (directory->name);
      fprintf (fp, "+%lu %lu %s\n" + ! directory->nfs,
	       (unsigned long) directory->device_number,
	       (unsigned long) directory->inode_number,
	       str ? str : directory->name);
      e = errno;
      if (str)
	free (str);
      errno = e;
    }

  return ! ferror (fp);
}

void
write_directory_file (void)
{
  FILE *fp = listed_incremental_stream;

  if (! fp)
    return;

  if (fseek (fp, 0L, SEEK_SET) != 0)
    seek_error (listed_incremental_option);
  if (sys_truncate (fileno (fp)) != 0)
    truncate_error (listed_incremental_option);

  fprintf (fp, "%lu\n", (unsigned long) start_time);
  if (! ferror (fp) && directory_table)
    hash_do_for_each (directory_table, write_directory_file_entry, fp);
  if (ferror (fp))
    write_error (listed_incremental_option);
  if (fclose (fp) != 0)
    close_error (listed_incremental_option);
}


/* Restoration of incremental dumps.  */

/* Examine the directories under directory_name and delete any
   files that were not there at the time of the back-up. */
void
purge_directory (char const *directory_name)
{
  char *archive_dir;
  char *current_dir;
  char *cur, *arc;
  size_t size;
  size_t copied;
  union block *data_block;
  char *to;

  current_dir = savedir (directory_name);

  if (!current_dir)
    {
      /* The directory doesn't exist now.  It'll be created.  In any
	 case, we don't have to delete any files out of it.  */

      skip_member ();
      return;
    }

  size = current_stat_info.stat.st_size;
  if (size != current_stat_info.stat.st_size)
    xalloc_die ();
  archive_dir = xmalloc (size);
  to = archive_dir;
  for (; size > 0; size -= copied)
    {
      data_block = find_next_block ();
      if (!data_block)
	{
	  ERROR ((0, 0, _("Unexpected EOF in archive")));
	  break;		/* FIXME: What happens then?  */
	}
      copied = available_space_after (data_block);
      if (copied > size)
	copied = size;
      memcpy (to, data_block->buffer, copied);
      to += copied;
      set_next_block_after ((union block *)
			    (data_block->buffer + copied - 1));
    }

  for (cur = current_dir; *cur; cur += strlen (cur) + 1)
    {
      for (arc = archive_dir; *arc; arc += strlen (arc) + 1)
	{
	  arc++;
	  if (!strcmp (arc, cur))
	    break;
	}
      if (*arc == '\0')
	{
	  struct stat st;
	  char *p = new_name (directory_name, cur);

	  if (deref_stat (true, p, &st))
	    {
	      stat_diag (p);
	      WARN((0, 0, _("%s: Not purging directory: unable to stat"),
		    quotearg_colon (p)));
	      continue; 
	    }
	  else if (one_file_system_option && st.st_dev != root_device)
	    {
	      WARN((0, 0,
		    _("%s: directory is on a different device: not purging"),
		    quotearg_colon (p)));
	      continue;
	    }
	    
	  if (! interactive_option || confirm ("delete", p))
	    {
	      if (verbose_option)
		fprintf (stdlis, _("%s: Deleting %s\n"),
			 program_name, quote (p));
	      if (! remove_any_file (p, RECURSIVE_REMOVE_OPTION))
		{
		  int e = errno;
		  ERROR ((0, e, _("%s: Cannot remove"), quotearg_colon (p)));
		}
	    }
	  free (p);
	}

    }
  free (current_dir);
  free (archive_dir);
}
