Blob Blame History Raw
/*
   tio.c - timed io functions
   This file is part of the nss-pam-ldapd library.

   Copyright (C) 2007-2014 Arthur de Jong

   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.1 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
*/

#include "config.h"

#ifdef HAVE_STDINT_H
#include <stdint.h>
#endif /* HAVE_STDINT_H */
#include <stdlib.h>
#include <unistd.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <errno.h>
#include <string.h>
#include <signal.h>
#include <stdio.h>
#include <limits.h>
#include <poll.h>
#include <time.h>

#include "tio.h"

/* for platforms that don't have ETIME use ETIMEDOUT */
#ifndef ETIME
#define ETIME ETIMEDOUT
#endif /* ETIME */

/* structure that holds a buffer
   the buffer contains the data that is between the application and the
   file descriptor that is used for efficient transfer
   the buffer is built up as follows:
   |.....********......|
         ^start        ^size
         ^--len--^           */
struct tio_buffer {
  uint8_t *buffer;
  size_t size;      /* the size of the buffer */
  size_t maxsize;   /* the maximum size of the buffer */
  size_t start;     /* the start of the data (before start is unused) */
  size_t len;       /* size of the data (from the start) */
};

/* structure that holds all the state for files */
struct tio_fileinfo {
  int fd;
  struct tio_buffer readbuffer;
  struct tio_buffer writebuffer;
  int readtimeout;
  int writetimeout;
  int read_resettable; /* whether the tio_reset() function can be called */
#ifdef DEBUG_TIO_STATS
  /* this is used to collect statistics on the use of the streams
     and can be used to tune the buffer sizes */
  size_t byteswritten;
  size_t bytesread;
#endif /* DEBUG_TIO_STATS */
};

/* some older versions of Solaris don't provide CLOCK_MONOTONIC but do have
   a CLOCK_HIGHRES that has the same properties we need */
#ifndef CLOCK_MONOTONIC
#ifdef CLOCK_HIGHRES
#define CLOCK_MONOTONIC CLOCK_HIGHRES
#endif /* CLOCK_HIGHRES */
#endif /* not CLOCK_MONOTONIC */

/* update the timeout to the value that is remaining before the deadline
   returns the number of milliseconds before the deadline (or a negative
   value of the deadline has expired) */
static inline int tio_time_remaining(struct timespec *deadline, int timeout)
{
  struct timespec tv;
  /* if this is the first call, set the deadline and return the full time */
  if ((deadline->tv_sec == 0) && (deadline->tv_nsec == 0))
  {
    if (clock_gettime(CLOCK_MONOTONIC, deadline) == 0)
    {
      deadline->tv_sec += timeout / 1000;
      deadline->tv_nsec += (timeout % 1000) * 1000000;
    }
    return timeout;
  }
  /* get the current time (fall back to full time on error) */
  if (clock_gettime(CLOCK_MONOTONIC, &tv))
    return timeout;
  /* calculate time remaining in milliseconds */
  return (deadline->tv_sec - tv.tv_sec) * 1000 +
         (deadline->tv_nsec - tv.tv_nsec) / 1000000;
}

/* open a new TFILE based on the file descriptor */
TFILE *tio_fdopen(int fd, int readtimeout, int writetimeout,
                  size_t initreadsize, size_t maxreadsize,
                  size_t initwritesize, size_t maxwritesize)
{
  struct tio_fileinfo *fp;
  fp = (struct tio_fileinfo *)malloc(sizeof(struct tio_fileinfo));
  if (fp == NULL)
    return NULL;
  fp->fd = fd;
  /* initialize read buffer */
  fp->readbuffer.buffer = (uint8_t *)malloc(initreadsize);
  if (fp->readbuffer.buffer == NULL)
  {
    free(fp);
    return NULL;
  }
  fp->readbuffer.size = initreadsize;
  fp->readbuffer.maxsize = maxreadsize;
  fp->readbuffer.start = 0;
  fp->readbuffer.len = 0;
  /* initialize write buffer */
  fp->writebuffer.buffer = (uint8_t *)malloc(initwritesize);
  if (fp->writebuffer.buffer == NULL)
  {
    free(fp->readbuffer.buffer);
    free(fp);
    return NULL;
  }
  fp->writebuffer.size = initwritesize;
  fp->writebuffer.maxsize = maxwritesize;
  fp->writebuffer.start = 0;
  fp->writebuffer.len = 0;
  /* initialize other attributes */
  fp->readtimeout = readtimeout;
  fp->writetimeout = writetimeout;
  fp->read_resettable = 0;
#ifdef DEBUG_TIO_STATS
  fp->byteswritten = 0;
  fp->bytesread = 0;
#endif /* DEBUG_TIO_STATS */
  return fp;
}

/* wait for any activity on the specified file descriptor using
   the specified deadline */
static int tio_wait(int fd, short events, int timeout,
                    struct timespec *deadline)
{
  int t;
  struct pollfd fds[1];
  int rv;
  while (1)
  {
    fds[0].fd = fd;
    fds[0].events = events;
    /* figure out the time we need to wait */
    if ((t = tio_time_remaining(deadline, timeout)) < 0)
    {
      errno = ETIME;
      return -1;
    }
    /* sanitiy check for moving clock */
    if (t > timeout)
      t = timeout;
    /* wait for activity */
    rv = poll(fds, 1, t);
    if (rv > 0)
      return 0; /* we have activity */
    else if (rv == 0)
    {
      /* no file descriptors were available within the specified time */
      errno = ETIME;
      return -1;
    }
    else if ((errno != EINTR) && (errno != EAGAIN))
      /* some error ocurred */
      return -1;
    /* we just try again on EINTR or EAGAIN */
  }
}

/* do a read on the file descriptor, returning the data in the buffer
   if no data was read in the specified time an error is returned */
int tio_read(TFILE *fp, void *buf, size_t count)
{
  struct timespec deadline = {0, 0};
  int rv;
  uint8_t *tmp;
  size_t newsz;
  size_t len;
  /* have a more convenient storage type for the buffer */
  uint8_t *ptr = (uint8_t *)buf;
  /* loop until we have returned all the needed data */
  while (1)
  {
    /* check if we have enough data in the buffer */
    if (fp->readbuffer.len >= count)
    {
      if (count > 0)
      {
        if (ptr != NULL)
          memcpy(ptr, fp->readbuffer.buffer + fp->readbuffer.start, count);
        /* adjust buffer position */
        fp->readbuffer.start += count;
        fp->readbuffer.len -= count;
      }
      return 0;
    }
    /* empty what we have and continue from there */
    if (fp->readbuffer.len > 0)
    {
      if (ptr != NULL)
      {
        memcpy(ptr, fp->readbuffer.buffer + fp->readbuffer.start,
               fp->readbuffer.len);
        ptr += fp->readbuffer.len;
      }
      count -= fp->readbuffer.len;
      fp->readbuffer.start += fp->readbuffer.len;
      fp->readbuffer.len = 0;
    }
    /* after this point until the read fp->readbuffer.len is 0 */
    if (!fp->read_resettable)
    {
      /* the stream is not resettable, re-use the buffer */
      fp->readbuffer.start = 0;
    }
    else if (fp->readbuffer.start >= (fp->readbuffer.size - 4))
    {
      /* buffer is running empty, try to grow buffer */
      if (fp->readbuffer.size < fp->readbuffer.maxsize)
      {
        newsz = fp->readbuffer.size * 2;
        if (newsz > fp->readbuffer.maxsize)
          newsz = fp->readbuffer.maxsize;
        tmp = realloc(fp->readbuffer.buffer, newsz);
        if (tmp != NULL)
        {
          fp->readbuffer.buffer = tmp;
          fp->readbuffer.size = newsz;
        }
      }
      /* if buffer still does not contain enough room, clear resettable */
      if (fp->readbuffer.start >= (fp->readbuffer.size - 4))
      {
        fp->readbuffer.start = 0;
        fp->read_resettable = 0;
      }
    }
    /* wait until we have input */
    if (tio_wait(fp->fd, POLLIN, fp->readtimeout, &deadline))
      return -1;
    /* read the input in the buffer */
    len = fp->readbuffer.size - fp->readbuffer.start;
#ifdef SSIZE_MAX
    if (len > SSIZE_MAX)
      len = SSIZE_MAX;
#endif /* SSIZE_MAX */
    rv = read(fp->fd, fp->readbuffer.buffer + fp->readbuffer.start, len);
    /* check for errors */
    if (rv == 0)
    {
      errno = ECONNRESET;
      return -1;
    }
    else if ((rv < 0) && (errno != EINTR) && (errno != EAGAIN))
      return -1;        /* something went wrong with the read */
    else if (rv > 0)
      fp->readbuffer.len = rv;  /* skip the read part in the buffer */
#ifdef DEBUG_TIO_STATS
    fp->bytesread += rv;
#endif /* DEBUG_TIO_STATS */
  }
}

/* Read and discard the specified number of bytes from the stream. */
int tio_skip(TFILE *fp, size_t count)
{
  return tio_read(fp, NULL, count);
}

/* Read all available data from the stream and empty the read buffer. */
int tio_skipall(TFILE *fp, int timeout)
{
  struct timespec deadline = {0, 0};
  int rv;
  size_t len;
  /* clear the read buffer */
  fp->readbuffer.start = 0;
  fp->readbuffer.len = 0;
  fp->read_resettable = 0;
  /* read until we can't read no more */
  len = fp->readbuffer.size;
#ifdef SSIZE_MAX
  if (len > SSIZE_MAX)
    len = SSIZE_MAX;
#endif /* SSIZE_MAX */
  while (1)
  {
    /* wait until we have input */
    if (tio_wait(fp->fd, POLLIN, timeout, &deadline))
      return -1;
    /* read data from the stream */
    rv = read(fp->fd, fp->readbuffer.buffer, len);
    if (rv == 0)
      return 0; /* end-of-file */
    if ((rv < 0) && (errno == EWOULDBLOCK))
      return 0; /* we've ready everything we can without blocking */
    if ((rv < 0) && (errno != EINTR) && (errno != EAGAIN))
      return -1; /* something went wrong with the read */
  }
}

/* the caller has assured us that we can write to the file descriptor
   and we give it a shot */
static int tio_writebuf(TFILE *fp)
{
  int rv;
  /* write the buffer */
#ifdef MSG_NOSIGNAL
  rv = send(fp->fd, fp->writebuffer.buffer + fp->writebuffer.start,
            fp->writebuffer.len, MSG_NOSIGNAL);
#else /* not MSG_NOSIGNAL */
  /* on platforms that cannot use send() with masked signals, we change the
     signal mask and change it back after the write (note that there is a
     race condition here) */
  struct sigaction act, oldact;
  /* set up sigaction */
  memset(&act, 0, sizeof(struct sigaction));
  act.sa_sigaction = NULL;
  act.sa_handler = SIG_IGN;
  sigemptyset(&act.sa_mask);
  act.sa_flags = SA_RESTART;
  /* ignore SIGPIPE */
  if (sigaction(SIGPIPE, &act, &oldact) != 0)
    return -1; /* error setting signal handler */
  /* write the buffer */
  rv = write(fp->fd, fp->writebuffer.buffer + fp->writebuffer.start,
             fp->writebuffer.len);
  /* restore the old handler for SIGPIPE */
  if (sigaction(SIGPIPE, &oldact, NULL) != 0)
    return -1; /* error restoring signal handler */
#endif
  /* check for errors */
  if ((rv == 0) || ((rv < 0) && (errno != EINTR) && (errno != EAGAIN)))
    return -1; /* something went wrong with the write */
  /* skip the written part in the buffer */
  if (rv > 0)
  {
    fp->writebuffer.start += rv;
    fp->writebuffer.len -= rv;
#ifdef DEBUG_TIO_STATS
    fp->byteswritten += rv;
#endif /* DEBUG_TIO_STATS */
    /* reset start if len is 0 */
    if (fp->writebuffer.len == 0)
      fp->writebuffer.start = 0;
    /* move contents of the buffer to the front if it will save enough room */
    if (fp->writebuffer.start >= (fp->writebuffer.size / 4))
    {
      memmove(fp->writebuffer.buffer,
              fp->writebuffer.buffer + fp->writebuffer.start,
              fp->writebuffer.len);
      fp->writebuffer.start = 0;
    }
  }
  return 0;
}

/* write all the data in the buffer to the stream */
int tio_flush(TFILE *fp)
{
  struct timespec deadline = {0, 0};
  /* loop until we have written our buffer */
  while (fp->writebuffer.len > 0)
  {
    /* wait until we can write */
    if (tio_wait(fp->fd, POLLOUT, fp->writetimeout, &deadline))
      return -1;
    /* write one block */
    if (tio_writebuf(fp))
      return -1;
  }
  return 0;
}

/* try a single write of data in the buffer if the file descriptor
   will accept data */
static int tio_flush_nonblock(TFILE *fp)
{
  struct pollfd fds[1];
  int rv;
  /* see if we can write without blocking */
  fds[0].fd = fp->fd;
  fds[0].events = POLLOUT;
  rv = poll(fds, 1, 0);
  /* check if any file descriptors were ready (timeout) or we were
     interrupted */
  if ((rv == 0) || ((rv < 0) && ((errno == EINTR) || (errno == EAGAIN))))
    return 0;
  /* any other errors? */
  if (rv < 0)
    return -1;
  /* so file descriptor will accept writes */
  return tio_writebuf(fp);
}

int tio_write(TFILE *fp, const void *buf, size_t count)
{
  size_t fr;
  uint8_t *tmp;
  size_t newsz;
  const uint8_t *ptr = (const uint8_t *)buf;
  /* keep filling the buffer until we have bufferred everything */
  while (count > 0)
  {
    /* figure out free size in buffer */
    fr = fp->writebuffer.size - (fp->writebuffer.start + fp->writebuffer.len);
    if (count <= fr)
    {
      /* the data fits in the buffer */
      memcpy(fp->writebuffer.buffer + fp->writebuffer.start +
             fp->writebuffer.len, ptr, count);
      fp->writebuffer.len += count;
      return 0;
    }
    else if (fr > 0)
    {
      /* fill the buffer with data that will fit */
      memcpy(fp->writebuffer.buffer + fp->writebuffer.start +
             fp->writebuffer.len, ptr, fr);
      fp->writebuffer.len += fr;
      ptr += fr;
      count -= fr;
    }
    /* try to flush some of the data that is in the buffer */
    if (tio_flush_nonblock(fp))
      return -1;
    /* if we have room now, try again */
    if (fp->writebuffer.size > (fp->writebuffer.start + fp->writebuffer.len))
      continue;
    /* try to grow the buffer */
    if (fp->writebuffer.size < fp->writebuffer.maxsize)
    {
      newsz = fp->writebuffer.size * 2;
      if (newsz > fp->writebuffer.maxsize)
        newsz = fp->writebuffer.maxsize;
      tmp = realloc(fp->writebuffer.buffer, newsz);
      if (tmp != NULL)
      {
        fp->writebuffer.buffer = tmp;
        fp->writebuffer.size = newsz;
        continue; /* try again */
      }
    }
    /* write the buffer to the stream */
    if (tio_flush(fp))
      return -1;
  }
  return 0;
}

int tio_close(TFILE *fp)
{
  int retv;
  /* write any buffered data */
  retv = tio_flush(fp);
#ifdef DEBUG_TIO_STATS
  /* dump statistics to stderr */
  fprintf(stderr, "DEBUG_TIO_STATS READ=%lu WRITTEN=%lu\n",
          (unsigned long)fp->bytesread, (unsigned long)fp->byteswritten);
#endif /* DEBUG_TIO_STATS */
  /* close file descriptor */
  if (close(fp->fd))
    retv = -1;
  /* free any allocated buffers */
  memset(fp->readbuffer.buffer, 0, fp->readbuffer.size);
  memset(fp->writebuffer.buffer, 0, fp->writebuffer.size);
  free(fp->readbuffer.buffer);
  free(fp->writebuffer.buffer);
  /* free the tio struct itself */
  free(fp);
  /* return the result of the earlier operations */
  return retv;
}

void tio_mark(TFILE *fp)
{
  /* move any data in the buffer to the start of the buffer */
  if ((fp->readbuffer.start > 0) && (fp->readbuffer.len > 0))
  {
    memmove(fp->readbuffer.buffer,
            fp->readbuffer.buffer + fp->readbuffer.start, fp->readbuffer.len);
    fp->readbuffer.start = 0;
  }
  /* mark the stream as resettable */
  fp->read_resettable = 1;
}

int tio_reset(TFILE *fp)
{
  /* check if the stream is (still) resettable */
  if (!fp->read_resettable)
    return -1;
  /* reset the buffer */
  fp->readbuffer.len += fp->readbuffer.start;
  fp->readbuffer.start = 0;
  return 0;
}