/* Unlink files.

   Copyright 2009, 2013-2014 Free Software Foundation, Inc.

   This file is part of GNU tar.

   GNU tar 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 tar 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 <system.h>
#include "common.h"
#include <quotearg.h>

struct deferred_unlink
  {
    struct deferred_unlink *next;   /* Next unlink in the queue */
    int dir_idx;                    /* Directory index in wd */
    char *file_name;                /* Name of the file to unlink, relative
				       to dir_idx */
    bool is_dir;                    /* True if file_name is a directory */
    off_t records_written;          /* Number of records written when this
				       entry got added to the queue */
  };

/* The unlink queue */
static struct deferred_unlink *dunlink_head, *dunlink_tail;

/* Number of entries in the queue */
static size_t dunlink_count;

/* List of entries available for allocation */
static struct deferred_unlink *dunlink_avail;

/* Delay (number of records written) between adding entry to the
   list and its actual removal. */
static size_t deferred_unlink_delay = 0;

static struct deferred_unlink *
dunlink_alloc (void)
{
  struct deferred_unlink *p;
  if (dunlink_avail)
    {
      p = dunlink_avail;
      dunlink_avail = p->next;
      p->next  = NULL;
    }
  else
    p = xmalloc (sizeof (*p));
  return p;
}

static void
dunlink_reclaim (struct deferred_unlink *p)
{
  free (p->file_name);
  p->next = dunlink_avail;
  dunlink_avail = p;
}

static void
flush_deferred_unlinks (bool force)
{
  struct deferred_unlink *p, *prev = NULL;
  int saved_chdir = chdir_current;

  for (p = dunlink_head; p; )
    {
      struct deferred_unlink *next = p->next;

      if (force
	  || records_written > p->records_written + deferred_unlink_delay)
	{
	  chdir_do (p->dir_idx);
	  if (p->is_dir)
	    {
	      const char *fname;

	      if (p->dir_idx
		  && (p->file_name[0] == 0
		      || strcmp (p->file_name, ".") == 0))
		{
		  fname = tar_dirname ();
		  chdir_do (p->dir_idx - 1);
		}
	      else
		fname = p->file_name;

	      if (unlinkat (chdir_fd, fname, AT_REMOVEDIR) != 0)
		{
		  switch (errno)
		    {
		    case ENOENT:
		      /* nothing to worry about */
		      break;
		    case ENOTEMPTY:
		      if (!force)
			{
			  /* Keep the record in list, in the hope we'll
			     be able to remove it later */
			  prev = p;
			  p = next;
			  continue;
			}
		      /* fall through */
		    default:
		      rmdir_error (fname);
		    }
		}
	    }
	  else
	    {
	      if (unlinkat (chdir_fd, p->file_name, 0) != 0 && errno != ENOENT)
		unlink_error (p->file_name);
	    }
	  dunlink_reclaim (p);
	  dunlink_count--;
	  p = next;
	  if (prev)
	    prev->next = p;
	  else
	    dunlink_head = p;
	}
      else
	{
	  prev = p;
	  p = next;
	}
    }
  if (!dunlink_head)
    dunlink_tail = NULL;
  chdir_do (saved_chdir);
}

void
finish_deferred_unlinks (void)
{
  flush_deferred_unlinks (true);
  while (dunlink_avail)
    {
      struct deferred_unlink *next = dunlink_avail->next;
      free (dunlink_avail);
      dunlink_avail = next;
    }
}

void
queue_deferred_unlink (const char *name, bool is_dir)
{
  struct deferred_unlink *p;

  if (dunlink_head
      && records_written > dunlink_head->records_written + deferred_unlink_delay)
    flush_deferred_unlinks (false);

  p = dunlink_alloc ();
  p->next = NULL;
  p->dir_idx = chdir_current;
  p->file_name = xstrdup (name);
  normalize_filename_x (p->file_name);
  p->is_dir = is_dir;
  p->records_written = records_written;

  if (dunlink_tail)
    dunlink_tail->next = p;
  else
    dunlink_head = p;
  dunlink_tail = p;
  dunlink_count++;
}
