/* GNU Mailutils -- a suite of utilities for electronic mail
   Copyright (C) 1999, 2000, 2001, 2005, 2006 Free Software Foundation, Inc.

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

   This library 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
   Lesser General Public License for more details.

   You should have received a copy of the GNU Lesser General
   Public License along with this library; if not, write to the
   Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
   Boston, MA 02110-1301 USA */

#ifdef HAVE_CONFIG_H
# include <config.h>
#endif

#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <fcntl.h>
#include <utime.h>

#include <sys/param.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>

#include <mailutils/errno.h>
#include <mailutils/locker.h>
#include <mailutils/mutil.h>

#define LOCKFILE_ATTR           0644

/* First draft by Brian Edmond. */
/* For subsequent modifications, see the GNU mailutils ChangeLog. */

struct _mu_locker
{
  int refcnt;

  char *file;
  int flags;
  int expire_time;
  int retries;
  int retry_sleep;

  union lock_data {
    struct {
      char *dotlock;
      char *nfslock;
    } dot;
    
    struct {
      char *name;
    } external;
    
    struct {
      int fd;
    } kernel;
  } data;
};

/* Assert that we're managing the refcnt and fd correctly, either
 * we have a lock, and the fd is valid, or refcnt is 0 and fd is -1.
 * And refcnt can never be less than 0.
 */
#define INVARIANT(l) assert((l)->refcnt >= 0);

static void expire_stale_lock (mu_locker_t lock);
static int stat_check (const char *file, int fd, int links);
static int check_file_permissions (const char *file);
static int lock_external (mu_locker_t l, int lock); 
static int _locker_lock_dotlock (mu_locker_t lock);
static int _locker_unlock_dotlock (mu_locker_t lock);
static int _locker_lock_kernel (mu_locker_t lock); 
static int _locker_unlock_kernel (mu_locker_t lock);

static int mu_locker_default_flags = MU_LOCKER_DEFAULT;
static time_t mu_locker_retry_timeout = MU_LOCKER_RETRY_SLEEP;
static size_t mu_locker_retry_count = MU_LOCKER_RETRIES;
static time_t mu_locker_expire_timeout = MU_LOCKER_EXPIRE_TIME;
static char *mu_locker_external_program = NULL;

int
mu_locker_set_default_flags (int flags, enum mu_locker_set_mode mode)
{
  switch (mode)
    {
    case mu_locker_assign:
      mu_locker_default_flags = flags;
      break;

    case mu_locker_set_bit:
      mu_locker_default_flags |= flags;
      break;

    case mu_locker_clear_bit:
      mu_locker_default_flags &= ~flags;
      break;

    default:
      return EINVAL;
    }
  return 0;
}

void
mu_locker_set_default_retry_timeout (time_t to)
{
  mu_locker_retry_timeout = to;
}

void
mu_locker_set_default_retry_count (size_t n)
{
  mu_locker_retry_count = n;
}

void
mu_locker_set_default_expire_timeout (time_t t)
{
  mu_locker_expire_timeout = t;
}

void
mu_locker_set_default_external_program (char *path)
{
  free (mu_locker_external_program);
  mu_locker_external_program = strdup (path);
}

int
mu_locker_create (mu_locker_t *plocker, const char *filename_, int flags)
{
  mu_locker_t l;
  char filename[_POSIX_PATH_MAX];
  int err = 0;

  if (plocker == NULL)
    return MU_ERR_OUT_PTR_NULL;

  if (filename_ == NULL)
    return EINVAL;

  if((err = mu_unroll_symlink(filename, sizeof(filename), filename_)))
    return err;

  l = calloc (1, sizeof (*l));

  if (l == NULL)
    return ENOMEM;

  l->file = strdup(filename);

  if (l->file == NULL)
    {
      free (l);
      return ENOMEM;
    }

  if (strcmp (filename, "/dev/null") == 0)
    l->flags = MU_LOCKER_NULL;
  else if (flags)
    l->flags = flags;
  else
    l->flags = mu_locker_default_flags;

  l->expire_time = mu_locker_expire_timeout;
  l->retries = mu_locker_retry_count;
  l->retry_sleep = mu_locker_retry_timeout;

  /* Initialize locker-type-specific data */
  if (l->flags & MU_LOCKER_EXTERNAL)
    {
      if (!(l->data.external.name = strdup (mu_locker_external_program ?
					    mu_locker_external_program :
					    MU_LOCKER_EXTERNAL_PROGRAM)))
	{
	  mu_locker_destroy (&l);
	  return ENOMEM;
	}
    }
  else if (!(l->flags & MU_LOCKER_KERNEL))
    {
      l->data.dot.dotlock = malloc (strlen (l->file)
				    + 5 /*strlen(".lock")*/ + 1);

      if (!l->data.dot.dotlock)
	{
	  free (l->file);
	  free (l);
	  return ENOMEM;
	}

      sprintf (l->data.dot.dotlock, "%s.lock", l->file);
    }

  INVARIANT(l);

  *plocker = l;

  return 0;
}

void
_locker_destroy_private (mu_locker_t locker)
{
  if (locker)
    {
      if (locker->flags & MU_LOCKER_EXTERNAL)
	free (locker->data.external.name);
      else if (locker->flags & MU_LOCKER_KERNEL)
	/* nothing */;
      else
	{
	  free (locker->data.dot.dotlock);
	  locker->data.dot.dotlock = NULL;
	  free (locker->data.dot.nfslock);
	  locker->data.dot.nfslock = NULL;
	}
    }
}

void
mu_locker_destroy (mu_locker_t *plocker)
{
  if (plocker && *plocker)
    {
      _locker_destroy_private (*plocker);
      free ((*plocker)->file);
      free (*plocker);
      *plocker = NULL;
    }
}

int
mu_locker_set_flags (mu_locker_t locker, int flags)
{
  if (!locker)
    return MU_ERR_LOCKER_NULL;

  locker->flags = flags;

  return 0;
}

int
mu_locker_set_expire_time (mu_locker_t locker, int etime)
{
  if (!locker)
    return MU_ERR_LOCKER_NULL;

  if (etime <= 0)
    return EINVAL;

  locker->expire_time = etime;

  return 0;
}

int
mu_locker_set_retries (mu_locker_t locker, int retries)
{
  if (!locker)
    return MU_ERR_LOCKER_NULL;

  if (retries <= 0)
    return EINVAL;

  locker->retries = retries;

  return 0;
}

int
mu_locker_set_retry_sleep (mu_locker_t locker, int retry_sleep)
{
  if (!locker)
    return MU_ERR_LOCKER_NULL;

  if (retry_sleep <= 0)
    return EINVAL;

  locker->retry_sleep = retry_sleep;

  return 0;
}

int
mu_locker_set_external (mu_locker_t locker, const char* program)
{
  char* p = NULL;

  if (!locker)
    return MU_ERR_LOCKER_NULL;
  if (!(locker->flags & MU_LOCKER_EXTERNAL))
    return EINVAL;
    
  /* program can be NULL */
  if (program != 0)
    {
      p = strdup (program);
      if (!p)
	return ENOMEM;
  }

  free (locker->data.external.name);
  locker->data.external.name = p;

  return 0;
}

int
mu_locker_get_flags (mu_locker_t locker, int *flags)
{
  if (!locker)
    return MU_ERR_LOCKER_NULL;

  if(!flags)
    return EINVAL;

  *flags = locker->flags;

  return 0;
}

int
mu_locker_get_expire_time (mu_locker_t locker, int *ptime)
{
  if (!locker)
    return MU_ERR_LOCKER_NULL;

  if (!ptime)
    return EINVAL;

  *ptime = locker->expire_time;

  return 0;
}

int
mu_locker_get_retries (mu_locker_t locker, int *retries)
{
  if (!locker)
    return MU_ERR_LOCKER_NULL;

  if (!retries)
    return EINVAL;

  *retries = locker->retries;

  return 0;
}

int
mu_locker_get_retry_sleep (mu_locker_t locker, int *retry_sleep)
{
  if (!locker)
    return MU_ERR_LOCKER_NULL;

  if (!retry_sleep)
    return EINVAL;

  *retry_sleep = locker->retry_sleep;

  return 0;
}

int
mu_locker_lock (mu_locker_t lock)
{
  int rc;
  int retries = 1;

  if (lock == NULL)
    return EINVAL;

  if (lock->flags == MU_LOCKER_NULL)
    return 0;
  
  INVARIANT (lock)
    /* Is the lock already applied? */
  if (lock->refcnt > 0)
    {
      lock->refcnt++;
      return 0;
    }

  if (access (lock->file, W_OK))
    {
      /* There is no use trying to lock the file if we are not
	 allowed to write to it */
      _locker_destroy_private (lock);
      lock->flags |= MU_LOCKER_NULL;
      return 0;
    }
  
  /* Check we are trying to lock a regular file, with a link count
     of 1, that we have permission to read, etc., or don't lock it. */
  if ((rc = check_file_permissions(lock->file)))
      return rc;

  /* Do the lock with an external program, if requested. */
  if (lock->flags & MU_LOCKER_EXTERNAL)
    return lock_external (lock, 1);
  else if (!(lock->flags & MU_LOCKER_KERNEL))
    {
      char *tmp, *p;

      tmp = strdup (lock->file);
      if (!tmp)
	return ENOMEM;

      strcpy (tmp, lock->file);
      p = strrchr (tmp, '/');
      if (!p)
	{
	  free (tmp);
	  tmp = strdup (".");
	  if (!tmp)
	    return ENOMEM;
	}
      else
	*p = 0; 

      if (access (tmp, W_OK))
	{
	  /* Fallback to kernel locking */
	  _locker_destroy_private (lock);
	  lock->flags |= MU_LOCKER_KERNEL;
	}
      free (tmp);
    }

  if (lock->flags & MU_LOCKER_RETRY)
    {
      retries = lock->retries;
    }

  while (retries--)
    {
      if (lock->flags & MU_LOCKER_KERNEL)
	rc = _locker_lock_kernel (lock);
      else
	rc = _locker_lock_dotlock (lock);
      if (rc == EAGAIN && retries)
	sleep (lock->retry_sleep);
      else if (rc)
	return rc;
      else /* rc == 0 */
	break;
    }

  lock->refcnt = 1;

  return 0;
}

int
mu_locker_touchlock (mu_locker_t lock)
{
  if (!lock)
    return MU_ERR_LOCKER_NULL;

  if (lock->flags == MU_LOCKER_NULL)
    return 0;
  
  if (lock->refcnt > 0)
    return utime (lock->data.dot.dotlock, NULL);

  return MU_ERR_LOCK_NOT_HELD;
}

int
mu_locker_unlock (mu_locker_t lock)
{
  int rc = 0;

  if (!lock)
    return MU_ERR_LOCKER_NULL;

  if (lock->flags == MU_LOCKER_NULL)
    return 0;

  if (lock->refcnt == 0)
    return MU_ERR_LOCK_NOT_HELD;

  /* Do the lock with an external program, if requested. */
  if (lock->flags & MU_LOCKER_EXTERNAL)
    return lock_external (lock, 0);
  
  if (lock->refcnt > 1)
    {
      lock->refcnt--;
      return 0;
    }

  if ((rc = check_file_permissions (lock->file)))
    return rc;

  if (lock->flags & MU_LOCKER_KERNEL)
    rc = _locker_unlock_kernel (lock);
  else
    rc = _locker_unlock_dotlock (lock);

  if (rc == 0)
    lock->refcnt = 0;

  return rc;
}

int
mu_locker_remove_lock (mu_locker_t lock)
{
  int err;

  if (!lock)
    return MU_ERR_LOCKER_NULL;

  if (lock->flags == MU_LOCKER_NULL)
    return 0;

  /* Force the reference count to 1 to unlock the file. */
  lock->refcnt = 1;
  err = mu_locker_unlock (lock);

  return err;
}

/* expire a stale lock (if MU_LOCKER_PID or MU_LOCKER_TIME) */
static void
expire_stale_lock (mu_locker_t lock)
{
  int stale = 0;
  int fd = open (lock->data.dot.dotlock, O_RDONLY);
  if (fd == -1)
    return;

  /* Check to see if this process is still running.  */
  if (lock->flags & MU_LOCKER_PID)
    {
      char buf[16];
      pid_t pid;
      int nread = read (fd, buf, sizeof (buf) - 1);
      if (nread > 0)
	{
	  buf[nread] = '\0';
	  pid = strtol (buf, NULL, 10);
	  if (pid > 0)
	    {
	      /* Process is gone so we try to remove the lock. */
	      if (kill (pid, 0) == -1)
		stale = 1;
	    }
	  else
	    stale = 1;		/* Corrupted file, remove the lock. */
	}
    }
  
  /* Check to see if the lock expired.  */
  if (lock->flags & MU_LOCKER_TIME)
    {
      struct stat stbuf;

      fstat (fd, &stbuf);
      /* The lock has expired. */
      if ((time (NULL) - stbuf.st_mtime) > lock->expire_time)
	stale = 1;
    }

  close (fd);
  if (stale)
    unlink (lock->data.dot.dotlock);
}

static int
stat_check (const char *file, int fd, int links)
{
  struct stat fn_stat;
  struct stat fd_stat;
  int err = 0;
  int localfd = -1;

  if (fd == -1)
    {
      localfd = open(file, O_RDONLY);
      
      if (localfd == -1)
	return errno;
      fd = localfd;
    }

  /* We should always be able to stat a valid fd, so this
     is an error condition. */
  if (lstat (file, &fn_stat) || fstat (fd, &fd_stat))
    err = errno;
  else
    {
      /* If the link and stat don't report the same info, or the
         file is a symlink, fail the locking. */
#define CHK(X) if(X) err = EINVAL

      CHK (!S_ISREG (fn_stat.st_mode));
      CHK (!S_ISREG (fd_stat.st_mode));
      CHK (fn_stat.st_nlink != links);
      CHK (fn_stat.st_dev != fd_stat.st_dev);
      CHK (fn_stat.st_ino != fd_stat.st_ino);
      CHK (fn_stat.st_mode != fd_stat.st_mode);
      CHK (fn_stat.st_nlink != fd_stat.st_nlink);
      CHK (fn_stat.st_uid != fd_stat.st_uid);
      CHK (fn_stat.st_gid != fd_stat.st_gid);
      CHK (fn_stat.st_rdev != fd_stat.st_rdev);

#undef CHK
    }
  if (localfd != -1)
    close (localfd);

  return err;
}

static int
check_file_permissions (const char *file)
{
  int fd = -1;
  int err = 0;

  if ((fd = open (file, O_RDONLY)) == -1)
    return errno == ENOENT ? 0 : errno;

  err = stat_check (file, fd, 1);
  close (fd);
  fd = -1;
  if (err)
    {
      if (err == EINVAL)
	err = MU_ERR_LOCK_BAD_FILE;
      return err;
    }

  return 0;
}

#ifndef MAXHOSTNAMELEN
# define MAXHOSTNAMELEN 256
#endif

/* Locker-specific lock/unlock functions */
int
_locker_lock_dotlock (mu_locker_t lock)
{
  char host[MAXHOSTNAMELEN + 1] = "localhost";
  char pid[11];		/* 10 is strlen(2^32 = 4294967296) */
  char now[11];
  size_t sz = 0;
  int err = 0;
  int fd;
    
  if (lock->data.dot.nfslock)
    {
      unlink (lock->data.dot.nfslock);
      free (lock->data.dot.nfslock);
      lock->data.dot.nfslock = 0;
    }

  expire_stale_lock (lock);

  /* build the NFS hitching-post to the lock file */

  gethostname (host, sizeof (host));
  host[MAXHOSTNAMELEN] = 0;

  snprintf (now, sizeof (now), "%lu", (unsigned long) time (0));
  now[sizeof (now) - 1] = 0;

  snprintf (pid, sizeof (pid), "%lu", (unsigned long) getpid ());
  pid[sizeof (pid) - 1] = 0;
		  
  sz = strlen (lock->file) + 1 /* "." */
    + strlen (pid) + 1 /* "." */
    + strlen (now) + 1 /* "." */
    + strlen (host) + 1;
  
  lock->data.dot.nfslock = malloc (sz);
  
  if (!lock->data.dot.nfslock)
    return ENOMEM;
  
  snprintf (lock->data.dot.nfslock, sz, "%s.%s.%s.%s", lock->file, pid, now,
	    host);
  
  fd = open (lock->data.dot.nfslock,
	     O_WRONLY | O_CREAT | O_EXCL, LOCKFILE_ATTR);
  if (fd == -1)
    {
      if (errno == EEXIST)
	return EAGAIN;
      else
	return errno;
    }
  close (fd);
  
  /* Try to link to the lockfile. */
  if (link (lock->data.dot.nfslock, lock->data.dot.dotlock) == -1)
    {
      unlink (lock->data.dot.nfslock);
      if (errno == EEXIST)
	return MU_ERR_LOCK_CONFLICT;
      return errno;
    }

  if ((fd = open (lock->data.dot.dotlock, O_RDWR)) == -1)
    {
      unlink (lock->data.dot.nfslock);
      return errno;
    }
  
  err = stat_check (lock->data.dot.nfslock, fd, 2);
  if (err)
    {
      unlink (lock->data.dot.nfslock);
      if (err == EINVAL)
	return MU_ERR_LOCK_BAD_LOCK;
      return errno;
    }

  unlink (lock->data.dot.nfslock);

  /* If no errors, we have the lock. */
  assert (lock->refcnt == 0);

  if (lock->flags & MU_LOCKER_PID)
    {
      char buf[16];
      sprintf (buf, "%ld", (long) getpid ());
      write (fd, buf, strlen (buf));
    }
  close(fd);
  return 0;
}  

int
_locker_unlock_dotlock (mu_locker_t lock)
{
  if (unlink (lock->data.dot.dotlock) == -1)
    {
      int err = errno;
      if (err == ENOENT)
	{
	  lock->refcnt = 0;
	  err = MU_ERR_LOCK_NOT_HELD;
	  return err;
	}
      return err;
    }
  return 0;
}

int
_locker_lock_kernel (mu_locker_t lock)
{
  int fd;
  struct flock fl;

  fd = open (lock->file, O_RDWR);
  if (fd == -1)
    return errno;
  lock->data.kernel.fd = fd;
  fl.l_type = F_WRLCK;
  fl.l_whence = SEEK_SET;
  fl.l_start = 0;
  fl.l_len = 0; /* Lock entire file */
  if (fcntl (fd, F_SETLK, &fl))
    {
#ifdef EACCESS      
      if (errno == EACCESS)
	return EAGAIN;
#endif
      if (errno == EAGAIN)
	return EAGAIN;
      return errno;
    }
  return 0;
}

int
_locker_unlock_kernel (mu_locker_t lock)
{
  struct flock fl;

  fl.l_type = F_UNLCK;
  fl.l_whence = SEEK_SET;
  fl.l_start = 0;
  fl.l_len = 0; /* Unlock entire file */
  if (fcntl (lock->data.kernel.fd, F_SETLK, &fl))
    {
#ifdef EACCESS
      if (errno == EACCESS)
	return EAGAIN;
#endif
      if (errno == EAGAIN)
	return EAGAIN;
      return errno;
    }
  close (lock->data.kernel.fd);
  return 0;
}

/*
  Estimate 1 decimal digit per 3 bits, + 1 for round off.
*/
#define DEC_DIGS_PER_INT (sizeof(int) * 8 / 3 + 1)

static int
lock_external (mu_locker_t l, int lock)
{
  int err = 0;
  char *av[6];
  int ac = 0;
  char aforce[3 + DEC_DIGS_PER_INT + 1];
  char aretry[3 + DEC_DIGS_PER_INT + 1];
  int status = 0;

  assert (l);
  assert (l->flags & MU_LOCKER_EXTERNAL);
  assert (lock == !l->refcnt);
  /* lock is true, refcnt is 0 or lock is false and refcnt is 1 */

  av[ac++] = l->data.external.name ?
                   l->data.external.name : MU_LOCKER_EXTERNAL_PROGRAM;

  if (l->flags & MU_LOCKER_TIME)
    {
      snprintf (aforce, sizeof (aforce), "-f%d", l->expire_time);
      aforce[sizeof (aforce) - 1] = 0;
      av[ac++] = aforce;
    }
  
  if (l->flags & MU_LOCKER_RETRY)
    {
      snprintf (aretry, sizeof (aretry), "-r%d", l->retries);
      aretry[sizeof (aretry) - 1] = 0;
      av[ac++] = aretry;
    }

  if (lock == 0)
    {
      av[ac++] = "-u";
    }

  av[ac++] = l->file;

  av[ac++] = NULL;

  if ((err = mu_spawnvp (av[0], av, &status)))
    return err;

  if (!WIFEXITED (status))
    {
      err = MU_ERR_LOCK_EXT_KILLED;
    }
  else
    {
      switch (WEXITSTATUS (status))
	{
	case 127:
	  err = MU_ERR_LOCK_EXT_FAIL;
	  break;
	case MU_DL_EX_OK:
	  err = 0;
	  l->refcnt = lock;
	  break;
	case MU_DL_EX_NEXIST:
	  err = MU_ERR_LOCK_NOT_HELD;
	  break;
	case MU_DL_EX_EXIST:
	  err = MU_ERR_LOCK_CONFLICT;
	  break;
	case MU_DL_EX_PERM:
	  err = EPERM;
	  break;
	default:
	case MU_DL_EX_ERROR:
	  err = MU_ERR_LOCK_EXT_ERR;
	  break;
	}
    }

  return err;
}