/* tarfs - A GNU tar filesystem for the Hurd.
   Copyright (C) 2002, 2003  Ludovic Courtès <ludo@chbouib.org>
 
   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 of the
   License, 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 <hurd.h>
#include <hurd/netfs.h>

#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <error.h>
#include <sys/mman.h>
#include <dirent.h>
#include <fcntl.h>
#include <limits.h>
#include <argp.h>
#include <argz.h>

#include "backend.h"
#include "tarfs.h"
#include "fs.h"
#include "cache.h"
#include "zipstores.h"
#include "debug.h"

/* New netfs variables */
char *netfs_server_name = "tarfs";
char *netfs_server_version = "(rw-alpha)";

/* Filesystem options */
struct tarfs_opts tarfs_options;
const char *argp_program_version =
  "tarfs(rw-alpha) for the GNU Hurd (compiled: " __DATE__ ")";

/* Argp data */
const char *argp_program_bug_address = "Ludovic Courtès <ludo@chbouib.org>";
const char *args_doc = "ARCHIVE";
const char *doc = "Hurd tar filesystem:\n"
   "parses a tar archive and creates the corresponding filesystem\n";

const struct argp_option fs_options[] =
{
#ifdef DEBUG
  { "debug",        'D', "FILE", 0, "Print debug output to FILE" },
#endif
  { "gzip",         'z', NULL, 0, "Archive file is gzipped" },
  { "bzip2",        'j', NULL, 0, "Archive file is bzip2'd" },
  { "no-timeout",   't', NULL, 0, "Parse file in a separate thread "
				  "(thus avoiding startup timeouts)" },
  { "readonly",     'r', NULL, 0, "Start tarfs read-only" },
  { "writable",     'w', NULL, 0, "Start tarfs writable (default)" },
  { "volatile",     'v', NULL, 0, "Start tarfs volatile "
  				  "(ie writable but not synced)" },
  { "create",       'c', NULL, 0, "Create tar file if not there" },
#if 0
  { "sync",         's', "INTERVAL", 0, "Sync all data not actually written "
				  "to disk every INTERVAL seconds (by "
				  "default, the data is *not* synced unless "
				  "explicitely requested)" },
#endif
  { 0 }
};

/* Tar file store & lock.  */
static struct store *tar_file;
static pthread_mutex_t  tar_file_lock;

/* Archive parsing hook (see tar.c) */
extern int (* tar_header_hook) (tar_record_t *, struct archive *);

/* List of tar items for this file */
static struct tar_list tar_list;

/* Data used by netfs_get_dirents() */
static struct node *curr_dir;
static struct node *curr_node;
static int    curr_entry = 0;

/* A convenience macro */
#define IF_RWFS \
  if (tarfs_options.readonly) \
    return EROFS;


#define D(_s) strdup(_s)

/* Open the tar file STORE according to TARFS_OPTIONS.  Assumes the
   store is already locked.  */
static error_t
open_store ()
{
  error_t err;
  int     flags = tarfs_options.readonly || tarfs_options.volatil
 		  ? STORE_READONLY
 		  : 0;

  switch (tarfs_options.compress)
  {
    case COMPRESS_NONE:
      err = store_file_open (tarfs_options.file_name, flags, &tar_file);
      break;
    case COMPRESS_GZIP:
      err = store_gzip_open (tarfs_options.file_name, flags, &tar_file);
      break;
    case COMPRESS_BZIP2:
      err = store_bzip2_open (tarfs_options.file_name, flags, &tar_file);
      break;
    default:
      error (1, EINVAL, "Compression method not implemented (yet)");
  }

  return err;
}

/* Close the tar file assuming that it is already locked.  */
static void
close_store ()
{
  store_free (tar_file);
  tar_file = NULL;
}

/* Reads NODE from file.  This is called by the cache backend.  */
static error_t
read_from_file (struct node *node, off_t offset, size_t howmuch,
                size_t *actually_read, void *data)
{
  error_t err = 0;
  store_offset_t start = NODE_INFO(node)->tar->offset;
  void *d = data;

  pthread_mutex_lock (&tar_file_lock);

  if (!tar_file)
    err = open_store ();

  if (!err)
    err = store_read (tar_file,
		      start + offset,
		      howmuch,
		      &d,
		      actually_read);

  pthread_mutex_unlock (&tar_file_lock);

  if (err)
    return err;

  assert (*actually_read <= howmuch);

  /* Checks whether store_read() has allocated a new buffer.  */
  if (data != d)
  {
    memcpy (data, d, *actually_read);
    munmap (d, *actually_read);
  }

  return 0;
}


/* Argp options parser.  */
error_t
tarfs_parse_opts (int key, char *arg, struct argp_state *state)
{
  switch (key)
  {
#ifdef DEBUG
    case 'D':
      debug_set_file (arg);
      break;
#endif
    case 'c':
      tarfs_options.create = 1;
      break;
    case 'v':
      tarfs_options.volatil = 1;
      break;
    case 'r':
      tarfs_options.readonly = 1;
      break;
    case 'w':
      tarfs_options.readonly = 0;
      break;
    case 't':
      tarfs_options.threaded = 1;
      break;
    case 'z':
      tarfs_options.compress = COMPRESS_GZIP;
      break;
    case 'j':
      tarfs_options.compress = COMPRESS_BZIP2;
      break;
    case 's':
      tarfs_options.interval = atoi (arg);
      break;
    case ARGP_KEY_ARG:
      tarfs_options.file_name = strdup (arg);
      if (!tarfs_options.file_name || !strlen (tarfs_options.file_name))
	argp_error (state, "No archive specified.");
  }

  return 0;
}

/* Returns the tarfs' struct argp.  */
void
tarfs_get_argp (struct argp *a)
{
  bzero (a, sizeof (struct argp));
  a->options  = fs_options;
  a->parser   = tarfs_parse_opts;
  a->args_doc = args_doc;
  a->doc      = doc;
}

/* Append to the malloced string *ARGZ of len *ARGZ_LEN a NULL-separated list
   of arguments.  */
error_t
tarfs_get_args (char **argz, size_t *argz_len)
{
  error_t err = 0;
  
  if (!err && tarfs_options.volatil)
    err = argz_add (argz, argz_len, "--volatile");
  else
  {
    if (tarfs_options.readonly)
      err = argz_add (argz, argz_len, "--readonly");
    else
      err = argz_add (argz, argz_len, "--writable");
  }

  if (err)
    return err;

  switch (tarfs_options.compress)
  {
    case COMPRESS_GZIP:
      err = argz_add (argz, argz_len, "--gzip");
      break;
    case COMPRESS_BZIP2:
      err = argz_add (argz, argz_len, "--bzip2");
      break;
  }

  if (err)
    return err;

  err = argz_add (argz, argz_len, tarfs_options.file_name);
  
  return err;
}

/* A basic set_options (). Only runtime options can be changed (using
   fsysopts): for instance, --no-timeout won't work (it doesn't make
   sense when tarfs is already running).  */
error_t
tarfs_set_options (const char *argz, size_t argz_len)
{
  error_t err = 0;

  /* If going readonly (resp. read-write) while the store is currently
     opened read-write (resp. read-only) then close it first.  */

  if (!strcmp (argz, "-r") || !strcmp (argz, "--readonly"))
  {
    if (!tarfs_options.readonly)
    {
      pthread_mutex_lock (&tar_file_lock);
      tarfs_options.readonly = 1;
      close_store ();
      err = open_store ();
      pthread_mutex_unlock (&tar_file_lock);

      if (err)
	tarfs_options.readonly = 0;
      else
        tarfs_options.volatil  = 0;
    }
  }
  else if (!strcmp (argz, "-w") || !strcmp (argz, "--writable"))
  {
    if (tarfs_options.readonly)
    {
      pthread_mutex_lock (&tar_file_lock);
      tarfs_options.readonly = 0;
      close_store ();
      err = open_store ();
      pthread_mutex_unlock (&tar_file_lock);

      if (err)
	tarfs_options.readonly = 1;
      else
        tarfs_options.volatil  = 0;
    }
  }
  else if (!strcmp (argz, "-v") || !strcmp (argz, "--volatile"))
    tarfs_options.readonly = 0, tarfs_options.volatil = 1;
  else
    err = EINVAL;

  return err;
}



error_t tarfs_create_node (struct node **newnode, struct node *dir,
			   const char *name, mode_t mode);

/* This function is called every time a header has been successfully parsed.
   It simply creates the node corresponding to the header.
   OFFSET denotes the offset of the header in the archive.  */
int
tarfs_add_header (tar_record_t *hdr, struct archive *archive)
{
  error_t err;
  static struct tar_item *last_item = NULL;
  struct node *dir, *new = NULL;
  char *name, *notfound, *retry;
  char arch_name[NAMSIZ + 1];
  
  assert (hdr != NULL);

  dir = netfs_root_node;

  memcpy (arch_name, hdr->header.arch_name, NAMSIZ);
  arch_name[NAMSIZ] = '\0';
  name = strdup (arch_name);
  assert (name);
  debug (("name = %s", name));

  /* Find the new node's parent directory.  */
  do
  {
    err = fs_find_node_path (&dir, &retry, &notfound, name);

    /* If a subdirectory wasn't found, then complain, create it and go on.
       eg.: if we wan't to create "/foo/bar" and "/foo" does not exist
            yet, then create "/foo" first and then continue with "bar".  */
    if (retry)
    {
      error (0, 0, "Inconsistent tar archive "
		   "(directory \"%s\" not found)", notfound);
      err = tarfs_create_node (&new, dir, notfound, S_IFDIR | 755);
      assert_perror (err);
      /*fs_make_subdir (&new, dir, notfound);
      NEW_NODE_INFO (new);*/
      free (name);
      name = retry;
      dir = new;
    }
  }
  while (retry);

  if (!notfound)
  {
    /* Means that this node is already here: do nothing.  Complain only
       if the node we found is not the root dir ("tar cf x ." creates
       './' as the first tar entry).  */
    if (dir != netfs_root_node)
      error (0, 0, "Warning: node \"%s\" already exists", name);

    return 0;
  }

  /* Now, go ahead and create the node.  */
  name = notfound;
  assert (strlen (name) > 0);

  switch (hdr->header.linkflag)
  {
    /* Hard link.  */
    case LF_LINK:
    {
      char *tgname;
      struct node *target;

      debug (("Hard linking \"%s\"", name));

      /* Get the target's node first. */
      tgname = malloc (NAMSIZ + 1);
      memcpy (tgname, hdr->header.arch_linkname, NAMSIZ);
      tgname [NAMSIZ] = '\0';
      target = netfs_root_node;
      fs_find_node_path (&target, &retry, &notfound, tgname);

      if ((!retry) && (!notfound))
      {
	/* FIXME: Call tarfs_create_node () and tarfs_link_node instead */
	fs_hard_link_node (&new, dir, name, target->nn_stat.st_mode, target);

	/* Update node info & stats */
	if (new)
	{
	  NEW_NODE_INFO (new);

          /* No need to create a cache for hard links.  */

	  /* Add the tar item into the list.  */
	  err = tar_make_item (&NODE_INFO(new)->tar, new,
			       0, archive->current_tar_position);
	  assert_perror (err);
	}
      }
      else
	error (0, 0, "Hard link target not found (%s -> %s)", name, tgname);

      break;
    }

    /* Other node types.  */
    default:
      err = fs_make_node (&new, dir, name, 0);
      assert_perror (err);

      /* Update node info & stats */
      if (new)
      {
	NEW_NODE_INFO (new);
	tar_fill_stat (archive, &new->nn_stat, hdr);

	/* Create a cache for the new node.  */
	err = cache_create (new);
	if (err)
	  error (1, err, "An error occured while creating the filesystem");

	/* Add the tar item into the list.  */
	err = tar_make_item (&NODE_INFO(new)->tar, new,
			     new->nn_stat.st_size, archive->current_tar_position);
	assert_perror (err);
      }
  }

  if (!new || err)
    error (1, err, "Filesystem could not be built");

  tar_insert_item (&tar_list, last_item, NODE_INFO(new)->tar);
  last_item = NODE_INFO(new)->tar;

#if 0
  debug (("%s: Created node \"%s\" (size=%li)\n",
          __FUNCTION__, name, new->nn_stat.st_size));
#endif

  /* Symlinks handling */
  if (S_ISLNK (new->nn_stat.st_mode))
  {
    if (hdr->header.arch_linkname[0])
      fs_link_node_path (new, strdup (hdr->header.arch_linkname));
    else
    {
      error (0, 0, "Warning: empty symlink target for node \"%s\"",
	     new->nn->name);
      fs_link_node_path (new, strdup (""));
    }
  }

  /* Directories */
  if (S_ISDIR (new->nn_stat.st_mode))
  {
    new->nn_stat.st_nlink = 2;
    new->nn->dir->nn_stat.st_nlink++;
  }

  return 0;
}


error_t
tarfs_init (struct node **root, struct iouser *user)
{
  error_t err = 0;
  io_statbuf_t st;
  int    flags;
  file_t tarfile;
  mode_t mode = 0644;

  /* Sync the archive.  */
  void
  sync_archive ()
  {
    error_t tarfs_sync_fs (int wait);

    while (1)
    {
      sleep (tarfs_options.interval);

      if (!tarfs_options.readonly)
	tarfs_sync_fs (0);
    }
  }

  /* Reads and parses a tar archive, possibly in a separate thread.  */
  void
  read_archive ()
  {
    error_t err;

    /* Go ahead: parse and build.  */
    pthread_mutex_lock (&tar_file_lock);
    err = tar_open_archive (tar_file);
    pthread_mutex_unlock (&tar_file_lock);

    if (err)
      error (1, 0, "Invalid tar archive (%s)", tarfs_options.file_name);
#if 0
    else
      {
        pthread_t t;
        pthread_create (&t, NULL, (void*(*)(void*)) sync_archive, NULL);
      }
#endif
  }


  /* Init. */
  if ((! tarfs_options.file_name) || (! strlen (tarfs_options.file_name)))
    error (1, 0, "No file specified");
  if (tarfs_options.create)
    tarfs_options.readonly = 0, flags = O_CREAT | O_READ | O_WRITE;
  else
    flags = tarfs_options.readonly || tarfs_options.volatil
            ? O_READ
            : O_READ | O_WRITE;

  /* Get the tarfile's stat.  */
  tarfile = file_name_lookup (tarfs_options.file_name, flags, mode);
  if (tarfile != MACH_PORT_NULL)
  {
    /* Check permissions */
    err = io_stat (tarfile, &st);

    if (!err && (flags & O_READ))
      err = fshelp_access (&st, S_IREAD, user);
    if (!err && (flags & O_WRITE))
      err = fshelp_access (&st, S_IWRITE, user);
  }
  else
    err = errno;

  if (err)
  {
    error (1, err, "%s", tarfs_options.file_name);
    return err;
  }
  mach_port_deallocate (mach_task_self (), tarfile);

  err = fs_init ();
  if (err)
    return err;

  /* Create root node */
  st.st_mode &= ~S_IFMT;
  st.st_mode |= S_IFDIR | S_IROOT | S_IATRANS;
  err = fs_make_node (&netfs_root_node, NULL, NULL, st.st_mode);
  if (err)
    return err;

  /* Parse the archive and build the filesystem */
  cache_init (read_from_file);
  tar_header_hook = tarfs_add_header;
  tar_list_init (&tar_list);

  /* Open the corresponding store */
  err = open_store ();
  if (err)
    error (1, err, "%s", tarfs_options.file_name);

  assert (tar_file);

  /* We make the following assumption because this is the way it's gotta
     be with these stores.  */
  assert (tar_file->block_size == 1);

  if (st.st_size)
  {
    if (tarfs_options.threaded)
      {
        pthread_t t;
        pthread_create(&t, NULL, (void*(*)(void*)) read_archive, NULL);
      }
    else
      read_archive ();
  }

  return 0;
}


int
tarfs_set_cd (struct node *dir)
{
  curr_dir = dir;
  curr_node = dir->nn->entries;

  /* Skip anonymous nodes (created by dir_mkfile ()).  */
  while (curr_node && (! curr_node->nn->name))
    curr_node = curr_node->next;

  curr_entry = 0;
  return 0;
}

int
tarfs_skip_entries (int n)
{
  assert (n >= 0);

  /* Skip N first DIR entries. */
  curr_node = curr_dir->nn->entries;

  if (n > 2)
  {
    /* Skip more than `.' and `..' */
    curr_entry = 2;
    while ((curr_entry < n) && (curr_node))
    {
      /* Skip anonymous nodes (created by dir_mkfile ()).  */
      do
	curr_node = curr_node->next;
      while (curr_node && (! curr_node->nn->name));

      curr_entry++;
    }
  }
  else
    curr_entry = n;
  
  /* Returns non-null if could not skip N entries. */
  return (curr_entry<=n)?0:1;
}

static inline int
_new_dirent (struct dirent** e, const struct node *n, const char* nodename)
{
  size_t namelen;
  char*  name;

  assert (nodename != NULL);

  /* N==NULL means that we are considering the node on which the
     translator is set.  */
  namelen = (n) ? strlen (nodename) : 2;

  /* Allocate it. */
  *e = mmap (NULL, sizeof (struct dirent) + namelen,
	     PROT_READ|PROT_WRITE, MAP_ANONYMOUS, 0, 0);
  assert (*e != NULL);
  
  /* Copy node name */
  name = &(*e)->d_name[0];

  if (n == NULL)
  {
    /* `..' */
    memcpy (name, nodename, 3);
    namelen = 2;
    (*e)->d_type = DT_DIR;
    (*e)->d_ino  = netfs_root_node->nn_stat.st_ino;
  }
  else
  {
    memcpy (name, nodename, namelen);

    /* Set the type corresponding to n->nn_stat.st_mode */
    if (n->nn_stat.st_mode & S_IFREG)
      (*e)->d_type = DT_REG;
    else if (n->nn_stat.st_mode & S_IFDIR)
      (*e)->d_type = DT_DIR;
    else if (n->nn_stat.st_mode & S_IFLNK)
      (*e)->d_type = DT_LNK;
    else
      (*e)->d_type = DT_UNKNOWN;

    /* if FILENO==0 then the node won't appear. */
    (*e)->d_fileno = n->nn_stat.st_ino;
  }

  assert (namelen != 0);

  (*e)->d_namlen = namelen;
  (*e)->d_reclen = sizeof (struct dirent) + namelen;

  return 0;
}

int
tarfs_get_next_entry (struct dirent **entry)
{
  switch (curr_entry++)
  {
    case 0:
      _new_dirent (entry, curr_dir, ".");
      break;
    case 1:
      _new_dirent (entry, curr_dir->nn->dir, "..");
      break;
    default:
      if (!curr_node)
	return 1;	/* no more entries */
      else
      {
	_new_dirent (entry, curr_node, curr_node->nn->name);

	/* Skip anonymous nodes (created by dir_mkfile ()).  */
	do
	  curr_node = curr_node->next;
	while (curr_node && (! curr_node->nn->name));
      }
      break;
  }

  return 0;
}

/* Looks up node named NAME and returns the result in NODE.  */
error_t
tarfs_lookup_node (struct node** node, struct node* dir, const char* name)
{
  struct node *n = dir->nn->entries;

  /* Look for NAME in DIR entries. */
  while (n)
  {
    char *this = n->nn->name;

    if (this)
      if (!strcmp (this, name))
        break;

    n = n->next;
  }

  *node = n;

  if (!n)
    return ENOENT;

  return 0;
}

error_t
tarfs_read_node (struct node *node, off_t offset, size_t *len, void* data)
{
  if (S_ISDIR (node->nn_stat.st_mode))
  {
    *len = 0;
    return EISDIR;
  }
  else
    return cache_read (node, offset, *len, data, len);
}


/* Write to NODE through its cache.  */
error_t
tarfs_write_node (struct node *node, off_t offset, size_t *len, const void *data)
{
  IF_RWFS;

  if (S_ISDIR (node->nn_stat.st_mode))
  {
    *len = 0;
    return EISDIR;
  }
  else
  {
    error_t err;
    /* Checks whether we need to actually write to another node.
       (hard links are not handled by cache_write ()).  */
    struct node *what = node->nn->hardlink ? node->nn->hardlink : node;
    
    err = cache_write (node, offset, data, *len, len);

    /* Synchronize stat with hard link's target.  */
    if ((! err) && (what != node))
      node->nn_stat.st_size = what->nn_stat.st_size;

    return err;
  }
}

/* Update NODE stat structure and mark it as dirty.  */
error_t
tarfs_change_stat (struct node *node, const io_statbuf_t *st)
{
  error_t err = 0;
  struct node *what = node->nn->hardlink ? node->nn->hardlink : node;

  IF_RWFS;

  if (st->st_size != what->nn_stat.st_size)
    /* Update the cache size */
    err = cache_set_size (what, st->st_size);

  if (!err)
  {
    what->nn_stat = *st;
    NODE_INFO(what)->stat_changed = 1;

    /* Synchronize NODE with its TARGET if it's a hard link.  */
    if (what != node)
    {
      node->nn_stat = what->nn_stat;
      NODE_INFO(node)->stat_changed = 1;
    }
  }

  return err;
}


/* Create a node named NAME in directory DIR. If NEWNODE is non-zero then
   it will point to the new node.  NAME is duplicated.  */
error_t
tarfs_create_node (struct node **newnode, struct node *dir,
		   const char *name, mode_t mode)
{
  error_t err;
  struct node *new = NULL;

  IF_RWFS;

  /* Allow anonymous (nameless) nodes (created by dir_mkfile ()).  */
  if (name)
  {
    /* NODE's path has to be at most NAMSIZ long. */
    char *path = fs_get_path_from_root (netfs_root_node, dir);
    if (strlen (name) + strlen (path) + 1 > NAMSIZ)
      return ENAMETOOLONG;

    debug (("Creating node %s", name));
  }
  else
  {
    debug (("Creating anonymous node", dir->nn->name));

    /* Don't add anonymous nodes into the tar list.  */
    err = fs_make_node (&new, dir, NULL, mode);

    if (!err && new)
      NEW_NODE_INFO (new);

    *newnode = new;

    err = cache_create (new);

    return err;
  }

  err = fs_make_node (&new, dir, strdup (name), mode);
  if (!err && new)
  {
    struct tar_item *tar, *prev_tar;

    NEW_NODE_INFO (new);
    err = cache_create (new);

    if (!err)
    {
      /* Insert a corresponding tar item into the list.
	 Offset `-1' denotes a note that does not exist inside the tar file.  */
      err = tar_make_item (&tar, new, 0, -1);
      assert_perror (err);

      /* Find a place to put TAR.  */
      tar_put_item (&prev_tar, tar);
      tar_insert_item (&tar_list, prev_tar, tar);
    }
  }

  if (newnode)
    *newnode = new;

  return err;
}

/* Unlink NODE.  NODE's tar_item will remain in the list until the filesystem
   is linked, *except* if its offset is `-1' (new node).  */
error_t
tarfs_unlink_node (struct node *node)
{
  error_t err = 0;
  struct tar_item *tar = NODE_INFO(node)->tar;

  IF_RWFS;

  debug (("Unlinking %s", node->nn->name));

  /* Delete NODE.  */
  err = fs_unlink_node (node);
  if (err)
    return err;

  /* If NODE has never existed inside the tar file, then remove its tar_item
     from the list.  */
  if (tar->offset == -1)
    tar_unlink_item (&tar_list, tar);

  return err;
}


void
tarfs_free_node (struct node *node)
{
  struct tar_item *tar = NODE_INFO (node)->tar;

  /* Free all related resources */
  cache_free (node);
  free (NODE_INFO (node));
  fs_free_node (node);
  tar->node = NULL;
}


/* Tries to create a hard link named NAME in DIR to file NODE.  */
error_t
tarfs_link_node (struct node *dir, struct node *target,
		 const char *name, int excl)
{
  error_t err = 0;
  struct tar_item *prev_tar, *tar;
  struct node *new;

  if (fs_find_node (dir, name))
    return excl ? EEXIST : 0;

  /* If the link's target is anonymous (nameless), then don't create
     a new node, just change its name.  */
  if (!target->nn->name)
  {
    new = target;
    new->nn->name = strdup (name);

    /* Insert NEW into the tar list */
    err = tar_make_item (&tar, new, 0, -1);
    if (!err)
      tar_put_item (&prev_tar, tar);
  }
  else
  {
    err = fs_hard_link_node (&new, dir, strdup (name),
			     target->nn_stat.st_mode, target);
    if (! err && new)
    {
      struct tar_item *t;
      NEW_NODE_INFO (new);

      /* Insert NEW into the tar list */
      err = tar_make_item (&tar, new, 0, -1);
      if (!err)
      {
	tar_put_item (&prev_tar, tar);

	/* Since NEW must appear after TARGET in the tar list,
	   Make sure that PREV_TAR comes *before* TARGET's tar, otherwise
	   set PREV_TAR to be TARGET's tar item.  */
	for (t = NODE_INFO(target)->tar;
	     t && (t != prev_tar);
	     t = t->next);

	if (!t)
	  prev_tar = NODE_INFO(new)->tar;
      }
    }
  }

  if (!err)
  {
    tar_insert_item (&tar_list, prev_tar, tar);
    NODE_INFO(new)->tar = tar;
  }

  return err;
}

/* Tries to turn NODE into a symlink to TARGET.  */
error_t
tarfs_symlink_node (struct node *node, const char *target)
{
  error_t err;

  err = fs_link_node_path (node, target);

  return err;
}

/* Tries to turn NODE into a device of type TYPE (either S_IFBLK or S_IFCHR).
 */
error_t
tarfs_mkdev_node (struct node *node, mode_t type, dev_t indexes)
{
  debug (("Not implemented"));
  return EOPNOTSUPP;
}


/* Rounds SIZE to the upper RECORDSIZE.  */
static inline size_t
round_size (size_t s)
{
   return  (RECORDSIZE * ( (s / RECORDSIZE) \
      			    + ((s % RECORDSIZE) ? 1 : 0 )) );
}

/* Cache nodes ahead CURR_TAR whose data reside in the region
   [OFFS, OFFS+SIZE] of the tar file.  */
static inline error_t
cache_ahead (struct tar_item *curr_tar, off_t offs, size_t size)
{
  error_t err = 0;
  struct node *node;	/* Corresponding node */
  size_t node_size;	/* Node size          */
  off_t  node_offs;	/* Node offset        */

  assert (size);

  do
  {
    /* Looks for an item available in the tar file.  */
    while (curr_tar)
      if ((curr_tar->offset != -1) && (curr_tar->node))
	break;
      else
        curr_tar = curr_tar->next;

    if (curr_tar)
    {
      node = curr_tar->node;
      node_offs = curr_tar->offset;
      node_size = node->nn_stat.st_size;

      /* If we are beyond NODE's boundary, assume it's already cached.  */
      if ( (offs < node_offs + node_size)
           && (offs + size > node_offs) )
      {
	/* Cache either the whole node or just what we need.  */
	size_t how_much;
	how_much = (offs + size > node_offs + node_size)
	           ? node_size
	           : offs + size - node_offs;

	debug (("Caching %i bytes from \"%s\"", how_much, node->nn->name));
	err = cache_cache (node, how_much);
	if (err)
	  return err;
      }

      curr_tar = curr_tar->next;
    }
  }
  while (curr_tar && (node_offs < offs + size));

  return err;
}

/* Store the filesystem into the tar file.  */
error_t
tarfs_sync_fs (int wait)
{
  error_t err = 0;
  char buf[RECORDSIZE];
  off_t  file_offs = 0; /* Current offset in the tar file */
  size_t orig_size = 0;	/* Total original tar file size */
  int need_trailing_block = 0; /* Do we need an additional block at the end? */
  struct tar_item *tar;

  /* Dump BUF to the tar file's store, enlarging it if necessary.  */
  error_t
  tar_write (off_t offset, void *buf, size_t len, size_t *amount)
  {
    error_t err = 0;
    int cnt = 0;

    while (1)
    {
      pthread_mutex_lock (&tar_file_lock);

      if (!tar_file)
	err = open_store ();

      if (!err)
	err = store_write (tar_file, offset, buf, len, amount);

      pthread_mutex_unlock (&tar_file_lock);

      cnt++;

      if (! err)
        break;
      if (cnt > 1)
        break;

      if (err == EIO)
      {
        /* Try to enlarge the file.  */
        debug (("Enlarging file from %lli to %lli",
               tar_file->size, offset + len));
        err = store_set_size (tar_file, offset + len);
        if (err)
          break;
      }
    }

    if (err)
      error (0, err,
	     "Could not write to file (offs="OFF_FMT")", file_offs);

    return err;
  }


  /* Traverse the tar items list and sync them.  */
  tar_list_lock (&tar_list);

  for (tar = tar_list_head (&tar_list);
       tar;
       /* TAR is incremented inside the loop */ )
  {
    struct node *node = tar->node;

    /* Compute the original tar file size.  */
    if (tar->offset != -1)
      orig_size += round_size (tar->orig_size) + RECORDSIZE;

    if (node)
    {
      int have_to_sync;
      char *path;
      size_t size;

      /* Lock the node first */
      pthread_mutex_lock (&node->lock);
      have_to_sync = (tar->offset != file_offs + RECORDSIZE);
      path = fs_get_path_from_root (netfs_root_node, node);
      size = node->nn_stat.st_size;

      /* Round SIZE.  */
      size = round_size (size);

      /* Synchronize NODE's stat.  */
      if ((NODE_INFO(node)->stat_changed) ||
	  (node->nn_stat.st_size != tar->orig_size) ||
	  (have_to_sync))
      {
	size_t amount;
	char *target;

	debug (("%s: syncing stat", path));

	/* Cache all the nodes that would have been overwritten otherwise.  */
	err = cache_ahead (tar, file_offs, RECORDSIZE);
	if (err)
	  break;

	target = node->nn->hardlink
		 ? fs_get_path_from_root (netfs_root_node, node->nn->hardlink)
		 : NULL;

	/* Create and write the corresponding tar header.  */
	tar_make_header ((tar_record_t *)buf, &node->nn_stat,
			 path, node->nn->symlink, target);

	err = tar_write (file_offs, buf, RECORDSIZE, &amount);
	if (err)
	  break;

	assert (amount == RECORDSIZE);

	/* Never finish the tar file with a stat record.  */
	need_trailing_block = 1;
      }
      file_offs  += RECORDSIZE;

      /* Synchronize NODE's contents except if it's a directory/link.  */
      if ((! S_ISDIR (node->nn_stat.st_mode))
          && (! node->nn->symlink)
          && (! node->nn->hardlink)
	  && ((! cache_synced (node)) || (have_to_sync)) )
      {
	off_t  start = file_offs; /* NODE's start */
	off_t  offs = 0; /* Offset in NODE */

	/* We don't need to cache_ahead () if we already are more than
	   one block behind the original item since we write only
	   RECORDSIZE bytes record.  */
	int ahead = (tar->offset - (long)start < RECORDSIZE);

	debug (("%s: syncing contents (%i bytes)", path, size));

	/* Write RECORDSIZE-long blocks.  */
	while (offs < size)
	{
	  size_t amount;
	  
	  if (ahead)
	  {
	    /* Cache everything that will be overlapped.  */
	    err = cache_ahead (tar, file_offs, RECORDSIZE);
	    if (err)
	      break;
	  }

	  err = cache_read (node, offs, RECORDSIZE, buf, &amount);
	  assert_perror (err);

	  /* Last block: fill it with zeros if necessary.  */
	  if (amount < RECORDSIZE)
	  {
	    assert (offs + RECORDSIZE == size);
	    bzero (&buf[amount], RECORDSIZE - amount);
	  }

	  /* Write the whole record, regardless of the amount of data
	     actually read.  */
	  err = tar_write (file_offs, buf, RECORDSIZE, &amount);
	  if (err)
	    break;

	  assert (amount == RECORDSIZE);

	  offs += RECORDSIZE;
	  file_offs += RECORDSIZE;
	}

	if (err)
	  break;

	/* Update NODE's offset *after* cache_ahead () !  */
	tar->offset = start;

	/* Update the original item size.  */
	tar->orig_size = node->nn_stat.st_size;

	/* If this is the last tar item, tell whether we need an additional
	   trailing block: if NODE's size is a RECORDSIZE multiple then we
	   need one.  */
	need_trailing_block = ! (tar->orig_size % RECORDSIZE);
      }
      else
      {
	/* Update NODE's offset.  */
	tar->offset = file_offs;

        /* Skip record anyway.  */
        file_offs += RECORDSIZE * ((node->nn_stat.st_size / RECORDSIZE)
                     + ( (node->nn_stat.st_size % RECORDSIZE) ? 1 : 0 ));
      }

      cache_free (node);
      free (path);
      pthread_mutex_unlock (&node->lock);

      /* Go to next item.  */
      tar = tar->next;
    }
    else
    {
      struct tar_item *next = tar->next;
      debug (("Node removed (size=%i)", tar->orig_size));
      tar_unlink_item_safe (&tar_list, tar);

      /* Go to next item.  */
      tar = next;
    }
  }

  tar_list_unlock (&tar_list);


  /* Add an empty record (FIXME: GNU tar added several of them) */
  if (!err)
  {
    size_t amount;

    if (!file_offs)
      error (0, 0, "Warning: archive is empty");

    bzero (buf, RECORDSIZE);
    err = tar_write (file_offs, buf, RECORDSIZE, &amount);

    if (err || (amount < RECORDSIZE))
    {
      if (!err)
        err = EIO;
    }

    file_offs += amount;
  }

  /* Checks whether the tar file needs to be truncated.  */
  if (!err && (file_offs < orig_size))
  {
    debug (("Truncating tar file from %u to "OFF_FMT" bytes",
            orig_size, file_offs));

    err = store_set_size (tar_file, file_offs);
    if (err)
      error (0, err, "Cannot truncate \"%s\"", tarfs_options.file_name);
  }


  if (!err)
  {
    /* Call store_free () to commit the changes. This is actually only useful
       for zip stores.  */
    close_store ();
  }

  return err;
}

/* Tarfs destructor.  */
error_t
tarfs_go_away ()
{
  error_t err;

  if (!tarfs_options.readonly && !tarfs_options.volatil)
  {
    err = tarfs_sync_fs (0);
    if (err)
      error (0, err, "Syncing failed");
  }

  if (tar_file)
    store_close_source (tar_file);

  debug (("Bye!"));

  return 0;
}


/* Defines the filesystem backend. */
struct fs_backend tarfs_backend =
{
  tarfs_init,
  tarfs_get_argp,
  tarfs_get_args,
  tarfs_set_options,
  tarfs_set_cd,
  tarfs_skip_entries,
  tarfs_get_next_entry,
  tarfs_lookup_node,
  tarfs_read_node,

  /* Write support */
  tarfs_write_node,
  tarfs_change_stat,
  tarfs_create_node,
  tarfs_unlink_node,

  tarfs_link_node,
  tarfs_symlink_node,
  tarfs_mkdev_node,

  tarfs_free_node,

  tarfs_sync_fs,
  tarfs_go_away
};
