/* estream.c - Extended stream I/O/ Library
   Copyright (C) 2004 g10 Code GmbH

   This file is part of Libestream.
 
   Libestream 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.
 
   Libestream 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 General Public License
   along with Libestream; if not, write to the Free Software
   Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
   02111-1307, USA.  */

#include <sys/types.h>
#include <sys/file.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <stdarg.h>
#include <fcntl.h>
#include <errno.h>
#include <stddef.h>

#include <pth.h>

#include "estream.h"



/* Generally used types.  */

typedef void *(*func_realloc_t) (void *mem, size_t size);
typedef void (*func_free_t) (void *mem);



/* Buffer management layer.  */

#define BUFFER_BLOCK_SIZE BUFSIZ

#define BUFFER_UNREAD_SIZE 16



/* Macros.  */

#define BUFFER_ROUND_TO_BLOCK(size, block_size) \
  (((size) + (block_size - 1)) / block_size)



/* Locking.  */

typedef pth_mutex_t estream_mutex_t;
#define ESTREAM_MUTEX_INITIALIZER PTH_MUTEX_INIT
#define ESTREAM_MUTEX_LOCK(mutex)        \
  pth_mutex_acquire (&(mutex), 0, NULL)
#define ESTREAM_MUTEX_UNLOCK(mutex)      \
  pth_mutex_release (&(mutex))
#define ESTREAM_MUTEX_TRYLOCK(mutex)     \
  ((pth_mutex_acquire (&(mutex), 1, NULL) == TRUE) ? 0 : -1)
#define ESTREAM_MUTEX_INITIALIZE(mutex)  \
  pth_mutex_init    (&(mutex))

#define ESTREAM_LOCK(stream)    ESTREAM_MUTEX_LOCK    (stream->internal->lock)
#define ESTREAM_UNLOCK(stream)  ESTREAM_MUTEX_UNLOCK  (stream->internal->lock)
#define ESTREAM_TRYLOCK(stream) ESTREAM_MUTEX_TRYLOCK (stream->internal->lock)

/* Primitive system I/O.  */

#define ESTREAM_SYS_READ  pth_read
#define ESTREAM_SYS_WRITE pth_write

/* Misc definitions.  */

#define ES_DEFAULT_OPEN_MODE (S_IRUSR | S_IWUSR)

/* An internal stream object.  */

struct estream_internal
{
  estream_mutex_t lock;		 /* Lock. */
  void *cookie;			 /* Cookie.               */
  void *opaque;			 /* Opaque data.          */
  unsigned int flags;		 /* Flags.                */
  off_t offset;
  es_cookie_read_function_t func_read;
  es_cookie_write_function_t func_write;
  es_cookie_seek_function_t func_seek;
  es_cookie_close_function_t func_close;
  int strategy;
  int fd;
  struct
  {
    unsigned int err: 1;
    unsigned int eof: 1;
  } indicators;
  unsigned int deallocate_buffer: 1;
};

/* Stream list.  */

typedef struct estream_list *estream_list_t;

struct estream_list
{
  estream_t car;
  estream_list_t cdr;
  estream_list_t *prev_cdr;
};

static estream_list_t estream_list;
static estream_mutex_t estream_list_lock = ESTREAM_MUTEX_INITIALIZER;

#define ESTREAM_LIST_LOCK   ESTREAM_MUTEX_LOCK   (estream_list_lock)
#define ESTREAM_LIST_UNLOCK ESTREAM_MUTEX_UNLOCK (estream_list_lock)



/* Macros.  */

#define ESTREAM_TMPFILE_TEMPLATE "/tmp/estream.XXXXXX"

/* Calculate array dimension.  */
#define DIM(array) (sizeof (array) / sizeof (*array))

/* Evaluate EXPRESSION, setting VARIABLE to the return code, if
   VARIABLE is zero.  */
#define SET_UNLESS_NONZERO(variable, tmp_variable, expression) \
  do                                                           \
    {                                                          \
      tmp_variable = expression;                               \
      if ((! variable) && tmp_variable)                        \
        variable = tmp_variable;                               \
    }                                                          \
  while (0)

/* List manipulation.  */

static int
es_list_add (estream_t estream)
{
  estream_list_t list_obj;
  int ret;

  list_obj = malloc (sizeof (*list_obj));
  if (! list_obj)
    ret = -1;
  else
    {
      ESTREAM_LIST_LOCK;
      list_obj->car = estream;
      list_obj->cdr = estream_list;
      list_obj->prev_cdr = &estream_list;
      if (estream_list)
	estream_list->prev_cdr = &list_obj->cdr;
      estream_list = list_obj;
      ESTREAM_LIST_UNLOCK;
      ret = 0;
    }

  return ret;
}

static void
es_list_remove (estream_t estream)
{
  estream_list_t list_obj;
  
  ESTREAM_LIST_LOCK;
  for (list_obj = estream_list; list_obj; list_obj = list_obj->cdr)
    if (list_obj->car == estream)
      {
	*list_obj->prev_cdr = list_obj->cdr;
	if (list_obj->cdr)
	  list_obj->cdr->prev_cdr = list_obj->prev_cdr;
	free (list_obj);
	break;
      }
  ESTREAM_LIST_UNLOCK;
}

typedef int (*estream_iterator_t) (estream_t stream);

static int
es_list_iterate (estream_iterator_t iterator)
{
  estream_list_t list_obj;
  int ret = 0;

  ESTREAM_LIST_LOCK;
  for (list_obj = estream_list; list_obj; list_obj = list_obj->cdr)
    ret |= (*iterator) (list_obj->car);
  ESTREAM_LIST_UNLOCK;

  return ret;
}



static int
es_init_do (void)
{
  int err, ret;
  
  ret = pth_init ();
  if (ret == TRUE)
    err = 0;
  else
    err = -1;

  return err;
}



/* Implementation of Memory I/O.  */

typedef struct estream_cookie_mem
{
  unsigned int flags;		/* Open flags.                 */
  char *memory;			/* Data.                       */
  size_t memory_size;		/* Size of MEMORY.             */
  size_t offset;		/* Current offset in MEMORY.   */
  size_t data_size;		/* Size of data in MEMORY.     */
  size_t block_size;		/* Block size.                 */
  unsigned int grow: 1;		/* MEMORY is allowed to grow.  */
  unsigned int append_zero: 1;	/* Append zero after data.     */
  unsigned int dont_free: 1;	/* Append zero after data.     */
  char **ptr;
  size_t *size;
  func_realloc_t func_realloc;
  func_free_t func_free;
} *estream_cookie_mem_t;

static int
es_func_mem_create (void **cookie,
		    unsigned char *data, size_t data_n, size_t data_size,
		    size_t block_size, unsigned int grow,
		    unsigned int append_zero, unsigned int dont_free,
		    char **ptr, size_t *size,
		    func_realloc_t func_realloc, func_free_t func_free,
		    unsigned int flags)
{
  estream_cookie_mem_t mem_cookie;
  int err;

  mem_cookie = malloc (sizeof (*mem_cookie));
  if (! mem_cookie)
    err = -1;
  else
    {
      mem_cookie->flags = flags;
      mem_cookie->memory = data;
      mem_cookie->memory_size = data_n;
      mem_cookie->offset = 0;
      mem_cookie->data_size = data_size;
      mem_cookie->block_size = block_size;
      mem_cookie->grow = grow ? 1 : 0;
      mem_cookie->append_zero = append_zero ? 1 : 0;
      mem_cookie->dont_free = dont_free ? 1 : 0;
      mem_cookie->ptr = ptr;
      mem_cookie->size = size;
      mem_cookie->func_realloc = func_realloc ? func_realloc : realloc;
      mem_cookie->func_free = func_free ? func_free : free;
      mem_cookie->offset = 0;
      *cookie = mem_cookie;
      err = 0;
    }

  return err;
}

static ssize_t
es_func_mem_read (void *cookie, char *buffer, size_t size)
{
  estream_cookie_mem_t mem_cookie = cookie;
  ssize_t ret;

  if (size > mem_cookie->data_size - mem_cookie->offset)
    size = mem_cookie->data_size - mem_cookie->offset;

  if (size)
    {
      memcpy (buffer, mem_cookie->memory + mem_cookie->offset, size);
      mem_cookie->offset += size;
    }
  
  ret = size;

  return ret;
}

static ssize_t
es_func_mem_write (void *cookie, const char *buffer, size_t size)
{
  estream_cookie_mem_t mem_cookie = cookie;
  func_realloc_t func_realloc = mem_cookie->func_realloc;
  char *memory_new;
  size_t newsize;
  ssize_t ret;
  int err;

  if (size)
    {
      /* Regular write.  */

      if (mem_cookie->flags & O_APPEND)
	/* Append to data.  */
	mem_cookie->offset = mem_cookie->data_size;
	  
      if (! mem_cookie->grow)
	if (size > mem_cookie->memory_size - mem_cookie->offset)
	  size = mem_cookie->memory_size - mem_cookie->offset;

      err = 0;

      while (size > (mem_cookie->memory_size - mem_cookie->offset))
	{
	  memory_new = (*func_realloc) (mem_cookie->memory,
					mem_cookie->memory_size
					+ mem_cookie->block_size);
	  if (! memory_new)
	    {
	      err = -1;
	      break;
	    }
	  else
	    {
	      if (mem_cookie->memory != memory_new)
		mem_cookie->memory = memory_new;
	      mem_cookie->memory_size += mem_cookie->block_size;
	    }
	}
      if (err)
	goto out;

      if (size)
	{
	  memcpy (mem_cookie->memory + mem_cookie->offset, buffer, size);
	  if (mem_cookie->offset + size > mem_cookie->data_size)
	    mem_cookie->data_size = mem_cookie->offset + size;
	  mem_cookie->offset += size;
	}
    }
  else
    {
      /* Flush.  */

      err = 0;
      if (mem_cookie->append_zero)
	{
	  if (mem_cookie->data_size >= mem_cookie->memory_size)
	    {
	      newsize = BUFFER_ROUND_TO_BLOCK (mem_cookie->data_size + 1,
					       mem_cookie->block_size) * mem_cookie->block_size;
	  
	      memory_new = (*func_realloc) (mem_cookie->memory, newsize);
	      if (! memory_new)
		{
		  err = -1;
		  goto out;
		}

	      if (mem_cookie->memory != memory_new)
		mem_cookie->memory = memory_new;
	      mem_cookie->memory_size = newsize;
	    }

	  mem_cookie->memory[mem_cookie->data_size + 1] = 0;
	}

      /* Return information to user if necessary.  */
      if (mem_cookie->ptr)
	*mem_cookie->ptr = (char *) mem_cookie->memory;
      if (mem_cookie->size)
	*mem_cookie->size = mem_cookie->data_size;
    }

 out:

  if (err)
    ret = -1;
  else
    ret = size;

  return ret;
}

static int
es_func_mem_seek (void *cookie, off_t *offset, int whence)
{
  estream_cookie_mem_t mem_cookie = cookie;
  off_t pos_new;
  int err = 0;

  switch (whence)
    {
    case SEEK_SET:
      pos_new = *offset;
      break;

    case SEEK_CUR:
      pos_new = mem_cookie->offset += *offset;
      break;

    case SEEK_END:
      pos_new = mem_cookie->data_size += *offset;
      break;

    default:
      /* Never reached.  */
      pos_new = 0;
    }

  if (pos_new >= mem_cookie->memory_size)
    {
      /* Grow buffer if possible.  */

      if (mem_cookie->grow)
	{
	  func_realloc_t func_realloc = mem_cookie->func_realloc;
	  size_t newsize;
	  void *p;

	  newsize = BUFFER_ROUND_TO_BLOCK (pos_new, mem_cookie->block_size);
	  p = (*func_realloc) (mem_cookie->memory, newsize);
	  if (! p)
	    {
	      err = -1;
	      goto out;
	    }
	  else
	    {
	      if (mem_cookie->memory != p)
		mem_cookie->memory = p;
	      mem_cookie->memory_size = newsize;
	    }
	}
      else
	{
	  errno = EINVAL;
	  err = -1;
	  goto out;
	}
    }

  if (pos_new > mem_cookie->data_size)
    /* Fill spare space with zeroes.  */
    memset (mem_cookie->memory + mem_cookie->data_size,
	    0, pos_new - mem_cookie->data_size);

  mem_cookie->offset = pos_new;
  *offset = pos_new;

 out:

  return err;
}

static int
es_func_mem_destroy (void *cookie)
{
  estream_cookie_mem_t mem_cookie = cookie;
  func_free_t func_free = mem_cookie->func_free;

  if (! mem_cookie->dont_free)
    (*func_free) (mem_cookie->memory);
  free (mem_cookie);

  return 0;
}

static es_cookie_io_functions_t estream_functions_mem =
  {
    es_func_mem_read,
    es_func_mem_write,
    es_func_mem_seek,
    es_func_mem_destroy,
  };

/* Implementation of FD I/O.  */

typedef struct estream_cookie_fd
{
  int fd;
} *estream_cookie_fd_t;

static int
es_func_fd_create (void **cookie, int fd, unsigned int flags)
{
  estream_cookie_fd_t fd_cookie;
  int err;

  fd_cookie = malloc (sizeof (*fd_cookie));
  if (! fd_cookie)
    err = -1;
  else
    {
      fd_cookie->fd = fd;
      *cookie = fd_cookie;
      err = 0;
    }
  
  return err;
}

static ssize_t
es_func_fd_read (void *cookie, char *buffer, size_t size)

{
  estream_cookie_fd_t file_cookie = cookie;
  ssize_t bytes_read;

  bytes_read = ESTREAM_SYS_READ (file_cookie->fd, buffer, size);

  return bytes_read;
}

static ssize_t
es_func_fd_write (void *cookie, const char *buffer, size_t size)
			   
{
  estream_cookie_fd_t file_cookie = cookie;
  ssize_t bytes_written;

  bytes_written = ESTREAM_SYS_WRITE (file_cookie->fd, buffer, size);

  return bytes_written;
}

static int
es_func_fd_seek (void *cookie, off_t *offset, int whence)
{
  estream_cookie_fd_t file_cookie = cookie;
  off_t offset_new;
  int err;

  offset_new = lseek (file_cookie->fd, *offset, whence);
  if (offset_new == -1)
    err = -1;
  else
    {
      *offset = offset_new;
      err = 0;
    }

  return err;
}

static int
es_func_fd_destroy (void *cookie)
{
  estream_cookie_fd_t fd_cookie = cookie;
  int err;

  if (fd_cookie)
    {
      err = close (fd_cookie->fd);
      free (fd_cookie);
    }
  else
    err = 0;

  return err;
}

static es_cookie_io_functions_t estream_functions_fd =
  {
    es_func_fd_read,
    es_func_fd_write,
    es_func_fd_seek,
    es_func_fd_destroy
  };

/* Implementation of File I/O.  */

static int
es_func_file_create (void **cookie, const char *path, unsigned int flags)
{
  estream_cookie_fd_t file_cookie;
  int err;
  int fd;

  err = 0;
  fd = -1;

  file_cookie = malloc (sizeof (*file_cookie));
  if (! file_cookie)
    {
      err = -1;
      goto out;
    }

  fd = open (path, flags, ES_DEFAULT_OPEN_MODE);
  if (fd == -1)
    {
      err = -1;
      goto out;
    }

  file_cookie->fd = fd;
  *cookie = file_cookie;

 out:

  if (err)
    free (file_cookie);

  return err;
}

static es_cookie_io_functions_t estream_functions_file =
  {
    es_func_fd_read,
    es_func_fd_write,
    es_func_fd_seek,
    es_func_fd_destroy
  };



/* Stream primitives.  */

static int
es_convert_mode (const char *mode, unsigned int *flags)
{
  struct
  {
    const char *mode;
    unsigned int flags;
  } mode_flags[] = { { "r",
		       O_RDONLY },
		     { "rb",
		       O_RDONLY },
		     { "w",
		       O_WRONLY | O_TRUNC | O_CREAT },
		     { "wb",
		       O_WRONLY | O_TRUNC | O_CREAT },
		     { "a",
		       O_WRONLY | O_APPEND | O_CREAT },
		     { "ab",
		       O_WRONLY | O_APPEND | O_CREAT },
		     { "r+",
		       O_RDWR },
		     { "rb+",
		       O_RDWR },
		     { "r+b",
		       O_RDONLY | O_WRONLY },
		     { "w+",
		       O_RDWR | O_TRUNC | O_CREAT },
		     { "wb+",
		       O_RDWR | O_TRUNC | O_CREAT },
		     { "w+b",
		       O_RDWR | O_TRUNC | O_CREAT },
		     { "a+",
		       O_RDWR | O_CREAT | O_APPEND },
		     { "ab+",
		       O_RDWR | O_CREAT | O_APPEND },
		     { "a+b",
		       O_RDWR | O_CREAT | O_APPEND } };
  unsigned int i;
  int err; 

  for (i = 0; i < DIM (mode_flags); i++)
    if (! strcmp (mode_flags[i].mode, mode))
      break;
  if (i == DIM (mode_flags))
    {
      errno = EINVAL;
      err = -1;
    }
  else
    {
      err = 0;
      *flags = mode_flags[i].flags;
    }

  return err;
}

static void
es_initialize (estream_t stream,
	       void *cookie, es_cookie_io_functions_t functions)
{
  stream->internal->cookie = cookie;
  stream->internal->opaque = NULL;
  stream->internal->offset = 0;
  stream->internal->func_read = functions.func_read;
  stream->internal->func_write = functions.func_write;
  stream->internal->func_seek = functions.func_seek;
  stream->internal->func_close = functions.func_close;
  stream->internal->strategy = _IOFBF;
  stream->internal->indicators.err = 0;
  stream->internal->indicators.eof = 0;
  stream->internal->deallocate_buffer = 0;

  stream->data_size = 0;
  stream->data_offset = 0;
  stream->data_flushed = 0;
  stream->unread_data_offset = 0;
  stream->dirty = 0;
}

static int
es_flush (estream_t stream)
{
  es_cookie_write_function_t func_write = stream->internal->func_write;
  int err;

  if (! func_write)
    {
      errno = EOPNOTSUPP;
      err = -1;
    }
  else if (stream->dirty && stream->data_offset)
    {
      size_t data_flushed = 0;
      size_t bytes_written;
      ssize_t ret;

      err = 0;
      /* Note: actually it should be enough to check for
	 "stream->data_offset - data_flushed" instead of
	 "(stream->data_offset - data_flushed) > 0", since a write
	 callback function should never write more than it is asked to
	 write.  But obviously glibc does it this way - FIXME?*/
      while (((stream->data_offset - data_flushed) > 0) && (! err))
	{
	  ret = (*func_write) (stream->internal->cookie,
			       stream->buffer + data_flushed,
			       stream->data_offset - data_flushed);
	  if (ret == -1)
	    {
	      bytes_written = 0;
	      err = -1;
	    }
	  else
	    bytes_written = ret;

	  data_flushed += bytes_written;
	  if (err)
	    break;
	}

      stream->data_flushed += data_flushed;
      if (stream->data_offset == data_flushed)
	{
	  stream->internal->offset += stream->data_offset;
	  stream->data_offset = 0;
	  stream->data_flushed = 0;
	  stream->dirty = 0;

	  /* Propagate flush event.  */
	  (*func_write) (stream->internal->cookie, NULL, 0);
	}
    }
  else
    err = 0;
    
  if (err)
    stream->internal->indicators.err = 1;

  return err;
}

static int
es_deinitialize (estream_t stream)
{
  es_cookie_close_function_t func_close;
  int err, tmp_err;

  func_close = stream->internal->func_close;

  err = 0;
  SET_UNLESS_NONZERO (err, tmp_err, es_flush (stream));
  if (func_close)
    SET_UNLESS_NONZERO (err, tmp_err, (*func_close) (stream->internal->cookie));
  
  /* Do not zero out "lock".  */
  memset (((unsigned char *) stream->internal) + offsetof (struct estream_internal, cookie), 0,
	  sizeof (struct estream_internal) - offsetof (struct estream_internal, cookie));

  /* Do not zero out "internal".  */
  memset (((unsigned char *) stream) + offsetof (struct estream_public, buffer), 0,
	  sizeof (struct estream_public) - offsetof (struct estream_public, buffer));

  return err;
}

static int
es_create (estream_t *stream, void *cookie, es_cookie_io_functions_t functions)
{
  estream_t stream_new = NULL;
  int initialized = 0;
  int err;

  stream_new = malloc (sizeof (*stream_new)
		       + sizeof (struct estream_internal)
		       + BUFFER_BLOCK_SIZE
		       + BUFFER_UNREAD_SIZE);
  if (! stream_new)
    {
      err = -1;
      goto out;
    }

  stream_new->internal = (void *) (((unsigned char *) stream_new)
				   + sizeof (*stream_new));
  stream_new->buffer = (void *) (((unsigned char *) stream_new)
				 + sizeof (*stream_new)
				 + sizeof (struct estream_internal));
  stream_new->buffer_size = BUFFER_BLOCK_SIZE;
  stream_new->unread_buffer = (void *) (((unsigned char *) stream_new)
					+ sizeof (*stream_new)
					+ sizeof (struct estream_internal)
					+ BUFFER_BLOCK_SIZE);
  stream_new->unread_buffer_size = BUFFER_UNREAD_SIZE;

  ESTREAM_MUTEX_INITIALIZE (stream_new->internal->lock);
  es_initialize (stream_new, cookie, functions);
  initialized = 1;

  err = es_list_add (stream_new);
  if (err)
    goto out;

  *stream = stream_new;

 out:

  if (err)
    {
      if (stream_new)
	{
	  if (initialized)
	    es_deinitialize (stream_new);
	  free (stream_new);
	}
    }

  return err;
}

static int
es_destroy (estream_t stream)
{
  int err = 0;

  if (stream)
    {
      es_list_remove (stream);
      err = es_deinitialize (stream);
      free (stream);
    }

  return err;
}

static void
es_empty (estream_t stream)
{
  stream->data_size = 0;
  stream->data_offset = 0;
  stream->unread_data_offset = 0;
}

static int
es_fill (estream_t stream)
{
  size_t bytes_read = 0;
  int err;

  if (! stream->internal->func_read)
    {
      errno = EOPNOTSUPP;
      err = -1;
    }
  else
    {
      es_cookie_read_function_t func_read = stream->internal->func_read;
      ssize_t ret;

      ret = (*func_read) (stream->internal->cookie,
			  stream->buffer, stream->buffer_size);
      if (ret == -1)
	{
	  bytes_read = 0;
	  err = -1;
	}
      else
	{
	  bytes_read = ret;
	  err = 0;
	}
    }

  if (err)
    stream->internal->indicators.err = 1;
  else if (! bytes_read)
    stream->internal->indicators.eof = 1;

  stream->internal->offset += stream->data_size;
  stream->data_size = bytes_read;
  stream->data_offset = 0;

  return err;
}

static int
es_read_nbf (estream_t stream,
	     unsigned char *buffer,
	     size_t bytes_to_read, size_t *bytes_read)
{
  es_cookie_read_function_t func_read = stream->internal->func_read;
  size_t data_read;
  ssize_t ret;
  int err;

  data_read = 0;
  err = 0;

  while (bytes_to_read - data_read)
    {
      ret = (*func_read) (stream->internal->cookie,
			  buffer + data_read, bytes_to_read - data_read);
      if (ret == -1)
	{
	  err = -1;
	  break;
	}
      else if (ret)
	data_read += ret;
      else
	break;
    }

  stream->internal->offset += data_read;
  *bytes_read = data_read;

  return err;
}

static int
es_read_fbf (estream_t stream,
	     unsigned char *buffer,
	     size_t bytes_to_read, size_t *bytes_read)
{
  size_t data_available;
  size_t data_to_read;
  size_t data_read;
  int err;

  data_read = 0;
  err = 0;

  while ((bytes_to_read - data_read) && (! err))
    {
      if (stream->data_offset == stream->data_size)
	{
	  /* Nothing more to read in current container, try to
	     fill container with new data.  */
	  err = es_fill (stream);
	  if (! err)
	    if (! stream->data_size)
	      /* Filling did not result in any data read.  */
	      break;
	}

      if (! err)
	{
	  /* Filling resulted in some new data.  */

	  data_to_read = bytes_to_read - data_read;
	  data_available = stream->data_size - stream->data_offset;
	  if (data_to_read > data_available)
	    data_to_read = data_available;

	  memcpy (buffer + data_read,
		  stream->buffer + stream->data_offset, data_to_read);
	  stream->data_offset += data_to_read;
	  data_read += data_to_read;
	}
    }

  *bytes_read = data_read;

  return err;
}

static int
es_read_lbf (estream_t stream,
	     unsigned char *buffer,
	     size_t bytes_to_read, size_t *bytes_read)
{
  int err;

  err = es_read_fbf (stream, buffer, bytes_to_read, bytes_read);

  return err;
}

static int
es_readn (estream_t stream,
	  unsigned char *buffer,
	  size_t bytes_to_read, size_t *bytes_read)
{
  size_t data_read_unread, data_read;
  int err;

  data_read_unread = 0;
  data_read = 0;
  err = 0;

  if (stream->dirty)
    /* Switching to reading mode -> flush output.  */
    es_flush (stream);

  /* Read unread data first.  */
  while ((bytes_to_read - data_read_unread) && stream->unread_data_offset)
    {
      buffer[data_read_unread] = stream->unread_buffer[stream->unread_data_offset - 1];
      stream->unread_data_offset--;
      data_read_unread++;
    }

  switch (stream->internal->strategy)
    {
    case _IONBF:
      err = es_read_nbf (stream,
			 buffer + data_read_unread,
			 bytes_to_read - data_read_unread, &data_read);
      break;
    case _IOLBF:
      err = es_read_lbf (stream,
			 buffer + data_read_unread,
			 bytes_to_read - data_read_unread, &data_read);
      break;
    case _IOFBF:
      err = es_read_fbf (stream,
			 buffer + data_read_unread,
			 bytes_to_read - data_read_unread, &data_read);
      break;
    }

  if (bytes_read)
    *bytes_read = data_read_unread + data_read;

  return err;
}

static void
es_unreadn (estream_t stream,
	    const unsigned char *data, size_t data_n, size_t *bytes_unread)
{
  size_t space_left;

  space_left = stream->unread_buffer_size - stream->unread_data_offset;

  if (data_n > space_left)
    data_n = space_left;

  if (! data_n)
    goto out;

  memcpy (stream->unread_buffer + stream->unread_data_offset, data, data_n);
  stream->unread_data_offset += data_n;
  stream->internal->indicators.eof = 0;

 out:

  if (bytes_unread)
    *bytes_unread = data_n;
}

static int
es_seek (estream_t stream, off_t offset, int whence, off_t *offset_new)
{
  es_cookie_seek_function_t func_seek = stream->internal->func_seek;
  int err, ret;
  off_t off;

  if (! func_seek)
    {
      errno = EOPNOTSUPP;
      err = -1;
      goto out;
    }
  
  /* Flush data first in order to prevent flushing it to the wrong
     offset.  */
  err = es_flush (stream);
  if (err)
    goto out;

  off = offset;

  if (whence == SEEK_CUR)
    /* FIXME: this seems to be correct, but I'm not so happy with this
       exception - isn't there a nicer way?  */
    off = off - stream->data_size + stream->data_offset;
  
  ret = (*func_seek) (stream->internal->cookie, &off, whence);
  if (ret == -1)
    {
      err = -1;
      goto out;
    }
  else
    err = 0;

  if (offset_new)
    *offset_new = off;

  /* FIXME: verify.  */

  /* Discard input data.  */
  es_empty (stream);

  stream->internal->indicators.eof = 0;
  stream->internal->offset = off;

 out:
  
  if (err)
    stream->internal->indicators.err = 1;

  return err;
}

static int
es_write_nbf (estream_t stream,
	      const unsigned char *buffer,
	      size_t bytes_to_write, size_t *bytes_written)
{
  es_cookie_write_function_t func_write = stream->internal->func_write;
  size_t data_written;
  ssize_t ret;
  int err;

  data_written = 0;
  err = 0;

  while (bytes_to_write - data_written)
    {
      ret = (*func_write) (stream->internal->cookie,
			   buffer + data_written, bytes_to_write - data_written);
      if (ret == -1)
	{
	  err = -1;
	  break;
	}
      else
	data_written += ret;
    }

  stream->internal->offset += data_written;
  *bytes_written = data_written;

  return err;
}

static int
es_write_fbf (estream_t stream,
	      const unsigned char *buffer,
	      size_t bytes_to_write, size_t *bytes_written)
{
  size_t space_available;
  size_t data_to_write;
  size_t data_written;
  int err;

  data_written = 0;
  err = 0;

  while ((bytes_to_write - data_written) && (! err))
    {
      if (stream->data_offset == stream->buffer_size)
	/* Container full, flush buffer.  */
	err = es_flush (stream);

      if (! err)
	{
	  /* Flushing resulted in empty container.  */
	  
	  data_to_write = bytes_to_write - data_written;
	  space_available = stream->buffer_size - stream->data_offset;
	  if (data_to_write > space_available)
	    data_to_write = space_available;
	      
	  memcpy (stream->buffer + stream->data_offset,
		  buffer + data_written, data_to_write);
	  stream->data_offset += data_to_write;
	  data_written += data_to_write;
	  if (! stream->dirty)
	    stream->dirty = 1;
	}
    }

  *bytes_written = data_written;

  return err;
}

static int
es_write_lbf (estream_t stream,
	      const unsigned char *buffer,
	      size_t bytes_to_write, size_t *bytes_written)
{
  size_t data_flushed;
  size_t data_buffered;
  unsigned int i;
  int err;

  err = 0;
  data_flushed = 0;
  data_buffered = 0;

  for (i = bytes_to_write; i > 0; i--)
    if (buffer[i - 1] == '\n')
      break;

  /* Found a newline, directly write up to (including) this
     character.  */
  err = es_flush (stream);
  if (err)
    goto out;
  err = es_write_nbf (stream, buffer, i, &data_flushed);
  if (err)
    goto out;

  /* Write remaining data fully buffered.  */
  err = es_write_fbf (stream, buffer + data_flushed,
		      bytes_to_write - data_flushed, &data_buffered);
  if (err)
    goto out;

 out:

  *bytes_written = data_flushed + data_buffered;

  return err;
}


static int
es_writen (estream_t stream,
	   const unsigned char *buffer,
	   size_t bytes_to_write, size_t *bytes_written)
{
  size_t data_written;
  int err;

  data_written = 0;
  err = 0;
  
  if ((! stream->dirty) && stream->data_size)
    {
      /* Switching to writing mode -> discard input data and seek to
	 position at which reading has stopped.  */

      err = es_seek (stream, 0, SEEK_CUR, NULL);
      if (err)
	goto out;
    }

  switch (stream->internal->strategy)
    {
    case _IONBF:
      err = es_write_nbf (stream, buffer, bytes_to_write, &data_written);
      break;

    case _IOLBF:
      err = es_write_lbf (stream, buffer, bytes_to_write, &data_written);
      break;

    case _IOFBF:
      err = es_write_fbf (stream, buffer, bytes_to_write, &data_written);
      break;
    }

 out:
    
  if (bytes_written)
    *bytes_written = data_written;

  return err;
}

static int
es_peek (estream_t stream, unsigned char **data, size_t *data_size)
{
  int err;

  if (stream->data_offset == stream->data_size)
    /* Refill container.  */
    err = es_fill (stream);
  else
    err = 0;

  if (! err)
    {
      if (data)
	*data = stream->buffer + stream->data_offset;
      if (data_size)
	*data_size = stream->data_size - stream->data_offset;
    }

  return err;
}

/* Skip SIZE bytes of input data contained in buffer.  */
static int
es_skip (estream_t stream, size_t size)
{
  int err;

  if (stream->data_offset + size > stream->data_size)
    {
      errno = EINVAL;
      err = -1;
    }
  else
    {
      stream->data_offset += size;
      err = 0;
    }

  return err;
}

static int
es_read_line (estream_t stream, size_t max_length,
	      char **line, size_t *line_length)
{
  size_t space_left;
  size_t line_size;
  estream_t line_stream;
  char *line_new;
  void *line_stream_cookie;
  char *newline;
  unsigned char *data;
  size_t data_size;
  int err;

  line_new = NULL;
  line_stream = NULL;
  line_stream_cookie = NULL;

  err = es_func_mem_create (&line_stream_cookie, NULL, 0, 0, BUFFER_BLOCK_SIZE,
			    1, 0, 0, NULL, 0, realloc, free, O_RDWR);
  if (err)
    goto out;

  err = es_create (&line_stream, line_stream_cookie, estream_functions_mem);
  if (err)
    goto out;

  space_left = max_length;
  line_size = 0;
  while (1)
    {
      if (max_length && (space_left == 1))
	break;

      err = es_peek (stream, &data, &data_size);
      if (err || (! data_size))
	break;

      if (data_size > (space_left - 1))
	data_size = space_left - 1;

      newline = memchr (data, '\n', data_size);
      if (newline)
	{
	  data_size = (newline - (char *) data) + 1;
	  err = es_write (line_stream, data, data_size, NULL);
	  if (! err)
	    {
	      space_left -= data_size;
	      line_size += data_size;
	      es_skip (stream, data_size);
	      break;
	    }
	}
      else
	{
	  err = es_write (line_stream, data, data_size, NULL);
	  if (! err)
	    {
	      space_left -= data_size;
	      line_size += data_size;
	      es_skip (stream, data_size);
	    }
	}
      if (err)
	break;
    }
  if (err)
    goto out;

  /* Complete line has been written to line_stream.  */
  
  if ((max_length > 1) && (! line_size))
    {
      stream->internal->indicators.eof = 1;
      goto out;
    }

  err = es_seek (line_stream, 0, SEEK_SET, NULL);
  if (err)
    goto out;

  if (! *line)
    {
      line_new = malloc (line_size + 1);
      if (! line_new)
	{
	  err = -1;
	  goto out;
	}
    }
  else
    line_new = *line;

  err = es_read (line_stream, line_new, line_size, NULL);
  if (err)
    goto out;

  line_new[line_size] = '\0';

  if (! *line)
    *line = line_new;
  if (line_length)
    *line_length = line_size;

 out:

  if (line_stream)
    es_destroy (line_stream);
  else if (line_stream_cookie)
    es_func_mem_destroy (line_stream_cookie);

  if (err)
    {
      if (! *line)
	free (line_new);
      stream->internal->indicators.err = 1;
    }

  return err;
}

static int
es_print (estream_t restrict stream, const char * restrict format, va_list ap)
{
  char data[BUFFER_BLOCK_SIZE];
  size_t bytes_written;
  size_t bytes_read;
  FILE *tmp_stream;
  int err;

  bytes_written = 0;
  tmp_stream = NULL;
  err = 0;
  
  tmp_stream = tmpfile ();
  if (err)
    goto out;

  err = vfprintf (tmp_stream, format, ap);
  if (err < 0)
    goto out;

  err = fseek (tmp_stream, 0, SEEK_SET);
  if (err)
    goto out;

  while (1)
    {
      bytes_read = fread (data, 1, sizeof (data), tmp_stream);
      if (ferror (tmp_stream))
	{
	  err = -1;
	  break;
	}

      err = es_writen (stream, data, bytes_read, NULL);
      if (err)
	break;
      else
	bytes_written += bytes_read;
      if (feof (tmp_stream))
	break;
    }
  if (err)
    goto out;

 out:

  if (tmp_stream)
    fclose (tmp_stream);

  return err ? -1 : bytes_written;
}

static void
es_set_indicators (estream_t stream, int err, int eof)
{
  if (err != -1)
    stream->internal->indicators.err = err ? 1 : 0;
  if (eof != -1)
    stream->internal->indicators.eof = eof ? 1 : 0;
}

static int
es_get_indicator (estream_t stream, int err, int eof)
{
  int ret = 0;
  
  if (err)
    ret = stream->internal->indicators.err;
  else if (eof)
    ret = stream->internal->indicators.eof;

  return ret;
}

static int
es_set_buffering (estream_t stream,
		  char * restrict buffer, int mode, size_t size)
{
  int err;

  /* Flush buffer or discard input data.  */
  if (stream->dirty)
    err = es_flush (stream);
  else
    {
      es_empty (stream);
      es_set_indicators (stream, -1, 0);
      err = 0;
    }
  if (err)
    goto out;
  
  /* Free old buffer in case that was allocated by this function.  */
  if (stream->internal->deallocate_buffer)
    {
      stream->internal->deallocate_buffer = 0;
      free (stream->buffer);
      stream->buffer = NULL;
    }

  if (mode == _IONBF)
    stream->buffer_size = 0;
  else
    {
      void *buffer_new;
      
      if (buffer)
	buffer_new = buffer;
      else
	{
	  buffer_new = malloc (size);
	  if (! buffer_new)
	    {
	      err = -1;
	      goto out;
	    }
	}

      stream->buffer = buffer_new;
      stream->buffer_size = size;
      if (! buffer)
	stream->internal->deallocate_buffer = 1;
    }
  stream->internal->strategy = mode;

 out:

  return err;
}

static off_t
es_offset_calculate (estream_t stream)
{
  off_t offset;

  offset = stream->internal->offset + stream->data_offset;

  return offset;
}

static void
es_opaque_ctrl (estream_t stream, void *opaque_new, void **opaque_old)
{
  if (opaque_old)
    *opaque_old = stream->internal->opaque;
  if (opaque_new)
    stream->internal->opaque = opaque_new;
}



/* API.  */

int
es_init (void)
{
  int err;

  err = es_init_do ();

  return err;
}

estream_t
es_fopen (const char * restrict path, const char * restrict mode)
{
  unsigned int flags;
  int create_called;
  estream_t stream;
  void *cookie;
  int err;

  stream = NULL;
  cookie = NULL;
  create_called = 0;

  err = es_convert_mode (mode, &flags);
  if (err)
    goto out;
  
  err = es_func_file_create (&cookie, path, flags);
  if (err)
    goto out;

  create_called = 1;
  err = es_create (&stream, cookie, estream_functions_file);
  if (err)
    goto out;

 out:
  
  if (err && create_called)
    (*estream_functions_file.func_close) (cookie);

  return stream;
}

estream_t
es_mopen (unsigned char *data, size_t data_n, size_t data_size,
	  unsigned int grow,
	  func_realloc_t func_realloc, func_free_t func_free,
	  const char * restrict mode)
{
  unsigned int flags;
  int create_called;
  estream_t stream;
  void *cookie;
  int err;

  /* FIXME: check for sanity.  */

  cookie = 0;
  stream = NULL;
  create_called = 0;
  
  err = es_convert_mode (mode, &flags);
  if (err)
    goto out;

  err = es_func_mem_create (&cookie, data, data_n, data_size,
			    BUFFER_BLOCK_SIZE, grow, 0, 0,
			    NULL, 0, func_realloc, func_free, flags);
  if (err)
    goto out;
  
  create_called = 1;
  err = es_create (&stream, cookie, estream_functions_mem);
  if (err)
    goto out;

 out:

  if (err && create_called)
    (*estream_functions_mem.func_close) (cookie);

  return stream;
}

estream_t
es_open_memstream (char **ptr, size_t *size)
{
  unsigned int flags;
  int create_called;
  estream_t stream;
  void *cookie;
  int err;

  /* FIXME: check for sanity.  */

  flags = O_RDWR;
  create_called = 0;
  stream = NULL;
  cookie = 0;
  
  err = es_func_mem_create (&cookie, NULL, 0, 0,
			    BUFFER_BLOCK_SIZE, 1, 1, 1,
			    ptr, size, realloc, free, flags);
  if (err)
    goto out;
  
  create_called = 1;
  err = es_create (&stream, cookie, estream_functions_mem);
  if (err)
    goto out;

 out:

  if (err && create_called)
    (*estream_functions_mem.func_close) (cookie);

  return stream;
}

estream_t
es_fopencookie (void *cookie,
		const char * restrict mode, es_cookie_io_functions_t functions)
{
  unsigned int flags;
  estream_t stream;
  int err;

  stream = NULL;
  flags = 0;
  
  err = es_convert_mode (mode, &flags);
  if (err)
    goto out;

  err = es_create (&stream, cookie, functions);
  if (err)
    goto out;

 out:

  return stream;
}

estream_t
es_fdopen (int filedes, const char *mode)
{
  unsigned int flags;
  int create_called;
  estream_t stream;
  void *cookie;
  int err;

  stream = NULL;
  cookie = NULL;
  create_called = 0;

  err = es_convert_mode (mode, &flags);
  if (err)
    goto out;

  err = es_func_fd_create (&cookie, filedes, flags);
  if (err)
    goto out;

  create_called = 1;
  err = es_create (&stream, cookie, estream_functions_fd);
  if (err)
    goto out;

 out:

  if (err && create_called)
    (*estream_functions_fd.func_close) (cookie);

  return stream;
}
  
estream_t
es_freopen (const char *path, const char *mode, estream_t stream)
{
  int err;

  if (stream)
    {
      unsigned int flags;
      int create_called;
      void *cookie;

      cookie = NULL;
      create_called = 0;
      
      ESTREAM_LOCK (stream);

      es_deinitialize (stream);

      err = es_convert_mode (mode, &flags);
      if (err)
	goto leave;
      
      err = es_func_file_create (&cookie, path, flags);
      if (err)
	goto leave;

      create_called = 1;
      es_initialize (stream, cookie, estream_functions_file);

    leave:

      if (err)
	{
	  if (create_called)
	    es_func_fd_destroy (cookie);
      
	  es_destroy (stream);
	  stream = NULL;
	}
      else
	ESTREAM_UNLOCK (stream);
    }
  else
    {
      /* FIXME?  We don't support re-opening at the moment.  */
      errno = EINVAL;
      es_deinitialize (stream);
      es_destroy (stream);
      stream = NULL;
    }

  return stream;
}

int
es_fclose (estream_t stream)
{
  int err;

  err = es_destroy (stream);

  return err;
}

int
es_fileno_unlocked (estream_t stream)
{
  int fd;

  /* FIXME: this is ugly.  */
  if (stream->internal->func_read == es_func_fd_read)
    fd = ((estream_cookie_fd_t) stream->internal->cookie)->fd;
  else
    fd = -1;

  return fd;
}

void
es_flockfile (estream_t stream)
{
  ESTREAM_LOCK (stream);
}

int
es_ftrylockfile (estream_t stream)
{
  int ret;

  ret = ESTREAM_TRYLOCK (stream);

  return ret;
}

void
es_funlockfile (estream_t stream)
{
  ESTREAM_UNLOCK (stream);
}

int
es_fileno (estream_t stream)
{
  int ret;

  ESTREAM_LOCK (stream);
  ret = es_fileno_unlocked (stream);
  ESTREAM_UNLOCK (stream);

  return ret;
}

int
es_feof_unlocked (estream_t stream)
{
  return es_get_indicator (stream, 0, 1);
}

int
es_feof (estream_t stream)
{
  int ret;

  ESTREAM_LOCK (stream);
  ret = es_feof_unlocked (stream);
  ESTREAM_UNLOCK (stream);

  return ret;
}

int
es_ferror_unlocked (estream_t stream)
{
  return es_get_indicator (stream, 1, 0);
}

int
es_ferror (estream_t stream)
{
  int ret;

  ESTREAM_LOCK (stream);
  ret = es_ferror_unlocked (stream);
  ESTREAM_UNLOCK (stream);

  return ret;
}

void
es_clearerr_unlocked (estream_t stream)
{
  es_set_indicators (stream, 0, 0);
}

void
es_clearerr (estream_t stream)
{
  ESTREAM_LOCK (stream);
  es_clearerr_unlocked (stream);
  ESTREAM_UNLOCK (stream);
}

int
es_fflush (estream_t stream)
{
  int err;
  
  if (stream)
    {
      ESTREAM_LOCK (stream);
      err = es_flush (stream);
      ESTREAM_UNLOCK (stream);
    }
  else
    err = es_list_iterate (es_fflush);

  return err ? EOF : 0;
}

int
es_fseek (estream_t stream, long int offset, int whence)
{
  int err;

  ESTREAM_LOCK (stream);
  err = es_seek (stream, offset, whence, NULL);
  ESTREAM_UNLOCK (stream);

  return err;
}

int
es_fseeko (estream_t stream, off_t offset, int whence)
{
  int err;
  
  ESTREAM_LOCK (stream);
  err = es_seek (stream, offset, whence, NULL);
  ESTREAM_UNLOCK (stream);

  return err;
}

long int
es_ftell (estream_t stream)
{
  long int ret;
  
  ESTREAM_LOCK (stream);
  ret = es_offset_calculate (stream);
  ESTREAM_UNLOCK (stream);

  return ret;
}

off_t
es_ftello (estream_t stream)
{
  off_t ret = -1;

  ESTREAM_LOCK (stream);
  ret = es_offset_calculate (stream);
  ESTREAM_UNLOCK (stream);

  return ret;
}

void
es_rewind (estream_t stream)
{
  ESTREAM_LOCK (stream);
  es_seek (stream, 0L, SEEK_SET, NULL);
  es_set_indicators (stream, 0, -1);
  ESTREAM_UNLOCK (stream);
}

int
_es_getc_underflow (estream_t stream)
{
  int err;
  unsigned char c;
  size_t bytes_read;

  err = es_readn (stream, &c, 1, &bytes_read);

  return (err || (! bytes_read)) ? EOF : c;
}

int
_es_putc_overflow (int c, estream_t stream)
{
  unsigned char d = c;
  int err;

  err = es_writen (stream, &d, 1, NULL);

  return err ? EOF : c;
}

int
es_fgetc (estream_t stream)
{
  int ret;
  
  ESTREAM_LOCK (stream);
  ret = es_getc_unlocked (stream);
  ESTREAM_UNLOCK (stream);

  return ret;
}

int
es_fputc (int c, estream_t stream)
{
  int ret;
  
  ESTREAM_LOCK (stream);
  ret = es_putc_unlocked (c, stream);
  ESTREAM_UNLOCK (stream);

  return ret;
}

int
es_ungetc (int c, estream_t stream)
{
  unsigned char data[] = { c };
  size_t data_unread;

  ESTREAM_LOCK (stream);
  es_unreadn (stream, data, sizeof (data), &data_unread);
  ESTREAM_UNLOCK (stream);

  return data_unread ? c : EOF;
}

int
es_read (estream_t stream,
	 char *buffer, size_t bytes_to_read, size_t *bytes_read)
{
  int err;

  if (bytes_to_read)
    {
      ESTREAM_LOCK (stream);
      err = es_readn (stream, buffer, bytes_to_read, bytes_read);
      ESTREAM_UNLOCK (stream);
    }
  else
    err = 0;

  return err;
}

int
es_write (estream_t stream,
	  const char *buffer, size_t bytes_to_write, size_t *bytes_written)
{
  int err;

  if (bytes_to_write)
    {
      ESTREAM_LOCK (stream);
      err = es_writen (stream, buffer, bytes_to_write, bytes_written);
      ESTREAM_UNLOCK (stream);
    }
  else
    err = 0;

  return err;
}

size_t
es_fread (void * restrict ptr, size_t size, size_t nitems,
	  estream_t  restrict stream)
{
  int err;
  size_t ret, bytes;

  if (size * nitems)
    {
      ESTREAM_LOCK (stream);
      err = es_readn (stream, ptr, size * nitems, &bytes);
      ESTREAM_UNLOCK (stream);

      ret = bytes / size;
    }
  else
    ret = 0;

  return ret;
}

size_t
es_fwrite (const void * restrict ptr, size_t size, size_t nitems,
	   estream_t  restrict stream)
{
  int err;
  size_t ret, bytes;

  if (size * nitems)
    {
      ESTREAM_LOCK (stream);
      err = es_writen (stream, ptr, size * nitems, &bytes);
      ESTREAM_UNLOCK (stream);

      ret = bytes / size;
    }
  else
    ret = 0;

  return ret;
}

char *
es_fgets (char * restrict s, int n, estream_t restrict stream)
{
  char *ret = NULL;
  
  if (n)
    {
      size_t length;
      int err;
      
      ESTREAM_LOCK (stream);
      err = es_read_line (stream, n, &s, &length);
      ESTREAM_UNLOCK (stream);
      if (! err)
	ret = s;
    }
  
  return ret;
}

int
es_fputs (const char * restrict s, estream_t restrict stream)
{
  size_t length;
  int err;

  length = strlen (s);
  ESTREAM_LOCK (stream);
  err = es_writen (stream, s, length, NULL);
  ESTREAM_UNLOCK (stream);

  return err ? EOF : 0;
}

ssize_t
es_getline (char **lineptr, size_t *n, estream_t stream)
{
  char *line = NULL;
  size_t line_n = 0;
  int err;

  ESTREAM_LOCK (stream);
  err = es_read_line (stream, 0, &line, &line_n);
  ESTREAM_UNLOCK (stream);
  if (err)
    goto out;

  if (*lineptr || *n)
    {
      if (*n < (line_n + 1))
	{
	  void *p;

	  /* FIXME: align?  */
	  p = realloc (*lineptr, line_n);
	  if (! p)
	    {
	      free (line);
	      err = -1;
	      goto out;
	    }
	  else
	    {
	      if (*lineptr != p)
		*lineptr = p;
	    }
	}

      memcpy (*lineptr, line, line_n + 1);
      if (*n != line_n)
	*n = line_n;
    }
  else
    {
      *lineptr = line;
      *n = line_n;
    }

 out:

  return err ? err : line_n;
}

int
es_vfprintf (estream_t restrict stream, const char *restrict format,
	     va_list ap)
{
  int ret;
  
  ESTREAM_LOCK (stream);
  ret = es_print (stream, format, ap);
  ESTREAM_UNLOCK (stream);

  return ret;
}

int
es_fprintf (estream_t restrict stream, const char * restrict format, ...)
{
  int ret;
  
  va_list ap;
  va_start (ap, format);
  ESTREAM_LOCK (stream);
  ret = es_print (stream, format, ap);
  ESTREAM_UNLOCK (stream);
  va_end (ap);

  return ret;
}

estream_t
es_tmpfile (void)
{
  char template[] = ESTREAM_TMPFILE_TEMPLATE;
  unsigned int flags;
  int create_called;
  estream_t stream;
  void *cookie;
  int err;
  int fd;

  create_called = 0;
  stream = NULL;
  flags = O_RDWR | O_TRUNC | O_CREAT; /* FIXME - correct?  */
  cookie = NULL;
  fd = -1;
  
  fd = mkstemp (template);
  if (fd == -1)
    {
      err = -1;
      goto out;
    }

  err = unlink (template);
  if (err == -1)
    goto out;
  
  err = es_func_fd_create (&cookie, fd, flags);
  if (err)
    goto out;

  create_called = 1;
  err = es_create (&stream, cookie, estream_functions_fd);
  if (err)
    goto out;

 out:

  if (err)
    {
      if (create_called)
	es_func_fd_destroy (cookie);
      else if (fd != -1)
	close (fd);
      stream = NULL;
    }
  
  return stream;
}

int
es_setvbuf (estream_t restrict stream,
	    char * restrict buf, int type, size_t size)
{
  int err;
  
  if (((type == _IOFBF) || (type == _IOLBF) || (type == _IONBF))
      && (! ((! size) && (type != _IONBF))))
    {
      ESTREAM_LOCK (stream);
      err = es_set_buffering (stream, buf, type, size);
      ESTREAM_UNLOCK (stream);
    }
  else
    {
      errno = EINVAL;
      err = -1;
    }

  return err;
}

void
es_setbuf (estream_t restrict stream, char * restrict buf)
{
  ESTREAM_LOCK (stream);
  es_set_buffering (stream, buf, buf ? _IOFBF : _IONBF, BUFSIZ);
  ESTREAM_UNLOCK (stream);
}

void
es_opaque_set (estream_t stream, void *opaque)
{
  ESTREAM_LOCK (stream);
  es_opaque_ctrl (stream, opaque, NULL);
  ESTREAM_UNLOCK (stream);
}

void *
es_opaque_get (estream_t stream)
{
  void *opaque;
  
  ESTREAM_LOCK (stream);
  es_opaque_ctrl (stream, NULL, &opaque);
  ESTREAM_UNLOCK (stream);

  return opaque;
}