fbuf: New data structure for buffered file I/O.
[pspp] / src / libpspp / fbuf.c
diff --git a/src/libpspp/fbuf.c b/src/libpspp/fbuf.c
new file mode 100644 (file)
index 0000000..a3758ce
--- /dev/null
@@ -0,0 +1,553 @@
+/* PSPP - a program for statistical analysis.
+   Copyright (C) 2017 Free Software Foundation, Inc.
+
+   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 3 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, see <http://www.gnu.org/licenses/>. */
+
+#include <config.h>
+
+#include "fbuf.h"
+
+#include <assert.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#include "libpspp/assertion.h"
+#include "libpspp/cast.h"
+
+#include "gl/intprops.h"
+#include "gl/minmax.h"
+#include "gl/xalloc.h"
+#include "gl/xsize.h"
+
+#define FBUF_SIZE 4096
+
+struct fbuf_class
+  {
+    int (*close) (struct fbuf *);
+
+    /* Reads up to N bytes from FBUF's underlying file descriptor into BUFFER.
+       Returns the number of bytes read, if successful, zero at end of file, or
+       a negative errno value on error. */
+    int (*read) (struct fbuf *fbuf, void *buffer, size_t n);
+
+    /* Writes the N bytes in BUFFER to FBUF's underlying file descriptor.  The
+     * caller guarantees N > 0.  Returns the number of bytes written, if
+     * successful, otherwise a negative errno value. */
+    int (*write) (struct fbuf *fbuf, const void *buffer, size_t n);
+
+    /* Seeks to byte offset OFFSET in FBUF's underlying file descriptor.
+       Returns 0 if successful, otherwise a positive errno value.  Returns
+       -ESPIPE if FBUF does not support positioning. */
+    int (*seek) (struct fbuf *fbuf, off_t offset);
+
+    /* Returns the current byte offset in FBUF's underlying file descriptor, or
+       a negative errno value on error.  Returns -ESPIPE
+       if FBUF does not support positioning. */
+    off_t (*tell) (struct fbuf *fbuf);
+
+    /* Returns the size of the file underlying FBUF, in bytes, or a negative
+       errno value on error.  Returns -ESPIPE if FBUF does not support
+       positioning. */
+    off_t (*get_size) (struct fbuf *fbuf);
+  };
+
+struct fbuf_fd
+  {
+    struct fbuf up;
+    int fd;
+  };
+
+static void
+fbuf_init (struct fbuf *fbuf, const struct fbuf_class *class, off_t offset)
+{
+  memset (fbuf, 0, sizeof *fbuf);
+  fbuf->class = class;
+  fbuf->buffer = xmalloc (FBUF_SIZE);
+  fbuf->offset = offset >= 0 ? offset : TYPE_MINIMUM (off_t);
+}
+
+/* Closes FBUF.  Returns 0 if successful, otherwise a positive errno value that
+   represents an error reading or writing the underlying fd (which could have
+   happened earlier or as part of the final flush implied by closing). */
+int
+fbuf_close (struct fbuf *fbuf)
+{
+  if (!fbuf)
+    return 0;
+
+  fbuf_flush (fbuf);
+  int status = fbuf->status;
+  int error = fbuf->class->close (fbuf);
+  return status ? status : error;
+}
+
+/* Returns FBUF's error status, which is 0 if no error has been recorded and
+   otherwise a positive errno value.  The error, if any, reflects difficulty
+   reading or writing the underlying fd.  */
+int
+fbuf_get_status (const struct fbuf *fbuf)
+{
+  return fbuf->status;
+}
+
+/* Clears any previously recorded error status. */
+void
+fbuf_clear_status (struct fbuf *fbuf)
+{
+  fbuf->status = 0;
+}
+
+/* Returns the length of the file backing FBUF, in bytes, or a negative errno
+   value on error.  A return value of -ESPIPE indicates that the underlying
+   file is not seekable, i.e. does not have a length. */
+off_t
+fbuf_get_size (const struct fbuf *fbuf_)
+{
+  struct fbuf *fbuf = CONST_CAST (struct fbuf *, fbuf_);
+  return fbuf->class->get_size (fbuf);
+}
+
+/* Returns true if FBUF is seekable, false otherwise. */
+int
+fbuf_is_seekable (const struct fbuf *fbuf)
+{
+  return fbuf_tell (fbuf) != -ESPIPE;
+}
+
+/* Attempts to flush any data buffered for writing to the underlying file.
+   Returns 0 if successful (which includes the case where FBUF is not in write
+   mode) or a positive errno value if there is a write error. */
+int
+fbuf_flush (struct fbuf *fbuf)
+{
+  for (;;)
+    {
+      assert (fbuf->write_tail <= fbuf->write_head);
+      int n = fbuf->write_head - fbuf->write_tail;
+      if (n <= 0)
+        return 0;
+
+      int retval = fbuf->class->write (fbuf, fbuf->write_tail, n);
+      if (retval < 0)
+        {
+          fbuf->status = -retval;
+          return fbuf->status;
+        }
+
+      fbuf->write_tail += n;
+      if (fbuf->offset >= 0)
+        fbuf->offset += n;
+      if (fbuf->write_tail >= fbuf->write_head)
+        {
+          fbuf->write_tail = fbuf->write_head = fbuf->buffer;
+          return 0;
+        }
+    }
+}
+
+/* Returns the byte offset in FBUF's file of the read byte to be read or
+   written, or a negative errno value if the offset cannot be determined.
+   Returns -ESPIPE if the underlying file is not seekable. */
+off_t
+fbuf_tell (const struct fbuf *fbuf_)
+{
+  struct fbuf *fbuf = CONST_CAST (struct fbuf *, fbuf_);
+
+  if (fbuf->offset < 0)
+    {
+      if (fbuf->offset != -ESPIPE)
+        fbuf->offset = fbuf->class->tell (fbuf);
+
+      if (fbuf->offset < 0)
+        return fbuf->offset;
+    }
+
+  return (fbuf->offset
+          - (fbuf->read_head - fbuf->read_tail)
+          + (fbuf->write_head - fbuf->write_tail));
+}
+
+/* Attempts to seek in FBUF such that the next byte to be read or written will
+   be at byte offset OFFSET.  Returns 0 if successful or a negative errno value
+   otherwise.  Returns -ESPIPE if the underlying file is not seekable. */
+int
+fbuf_seek (struct fbuf *fbuf, off_t offset)
+{
+  if (offset < 0)
+    return EINVAL;
+
+  int error = fbuf_flush (fbuf);
+  if (error)
+    return error;
+
+  fbuf->read_tail = fbuf->read_head = NULL;
+  fbuf->write_tail = fbuf->write_head = fbuf->write_end = NULL;
+
+  error = fbuf->class->seek (fbuf, offset);
+  if (!error)
+    fbuf->offset = offset;
+  return error;
+}
+
+/* Attempts to write the SIZE bytes of data in DATA to FBUF.  On success,
+   returns the number of bytes actually written (possibly less than SIZE), and
+   on failure returns a negative errno value.  Returns 0 only if SIZE is 0.
+
+   If the last I/O operation on FBUF was a read, the caller must call
+   fbuf_seek() before this function. */
+ssize_t
+fbuf_write (struct fbuf *fbuf, const void *data_, size_t size)
+{
+  const uint8_t *data = data_;
+  size_t n_written = 0;
+  while (size > 0)
+    {
+      size_t avail = fbuf->write_end - fbuf->write_head;
+      size_t chunk = MIN (avail, size);
+      if (chunk)
+        {
+          if (chunk < FBUF_SIZE)
+            {
+              /* Normal case: copy into buffer. */
+              memcpy (fbuf->write_head, data, chunk);
+              fbuf->write_head += chunk;
+            }
+          else
+            {
+              /* Buffer is empty and we're writing more data than will fit in
+                 the buffer.  Skip the buffer. */
+              chunk = MIN (INT_MAX, size);
+              int retval = fbuf->class->write (fbuf, data, chunk);
+              if (retval < 0)
+                return n_written ? n_written : -retval;
+              if (fbuf->offset >= 0)
+                fbuf->offset += retval;
+            }
+          data += chunk;
+          size -= chunk;
+          n_written += chunk;
+        }
+      else
+        {
+          int error = fbuf_flush (fbuf);
+          if (error)
+            return n_written ? n_written : -error;
+
+          /* Use fbuf_seek() to switch between reading and writing. */
+          assert (!fbuf->read_head);
+
+          if (!fbuf->write_tail)
+            {
+              fbuf->write_tail = fbuf->write_head = fbuf->buffer;
+              fbuf->write_end = fbuf->buffer + FBUF_SIZE;
+            }
+        }
+    }
+  return n_written;
+}
+
+int
+fbuf_getc__ (struct fbuf *fbuf)
+{
+  uint8_t c;
+  int retval = fbuf_read (fbuf, &c, 1);
+  return retval == 1 ? c : EOF;
+}
+
+/* Attempts to read SIZE bytes of data from FBUF into DATA.  On success,
+   returns the number of bytes actually read (possibly less than SIZE), and on
+   failure returns a negative errno value.  Returns 0 only if end of file was
+   reached before any data could be read.
+
+   If the last I/O operation on FBUF was a write, the caller must call
+   fbuf_seek() before this function. */
+ssize_t
+fbuf_read (struct fbuf *fbuf, void *data_, size_t size)
+{
+  uint8_t *data = data_;
+  size_t n_read = 0;
+  while (size > 0)
+    {
+      size_t avail = fbuf->read_head - fbuf->read_tail;
+      size_t chunk = MIN (avail, size);
+      if (chunk)
+        {
+          /* Copy out of buffer. */
+          memcpy (data, fbuf->read_tail, chunk);
+          fbuf->read_tail += chunk;
+          data += chunk;
+          size -= chunk;
+          n_read += chunk;
+        }
+      else
+        {
+          /* Buffer is empty. */
+
+          /* Use fbuf_seek() to switch between reading and writing. */
+          assert (!fbuf->write_head);
+
+          if (size < FBUF_SIZE)
+            {
+              /* Normal case: fill the buffer. */
+              int retval = fbuf->class->read (fbuf, fbuf->buffer, FBUF_SIZE);
+              if (retval < 0)
+                {
+                  fbuf->status = -retval;
+                  return n_read ? n_read : retval;
+                }
+              else if (retval == 0)
+                return n_read;
+              if (fbuf->offset >= 0)
+                fbuf->offset += retval;
+              fbuf->read_tail = fbuf->buffer;
+              fbuf->read_head = fbuf->buffer + retval;
+            }
+          else
+            {
+              /* Caller's read buffer is bigger than FBUF_SIZE.  Use it
+                 directly. */
+              int retval = fbuf->class->read (fbuf, data, size);
+              if (retval < 0)
+                {
+                  fbuf->status = -retval;
+                  return n_read ? n_read : retval;
+                }
+              else if (retval == 0)
+                return n_read;
+              if (fbuf->offset >= 0)
+                fbuf->offset += retval;
+              data += retval;
+              size -= retval;
+              n_read += retval;
+            }
+        }
+    }
+  return n_read;
+}
+\f
+/* Implementation of file-based fbuf. */
+
+static const struct fbuf_class fbuf_fd_class;
+
+/* Returns a new fbuf that represents FD. */
+struct fbuf *
+fbuf_open_fd (int fd)
+{
+  struct fbuf_fd *fbuf = xmalloc (sizeof *fbuf);
+  fbuf_init (&fbuf->up, &fbuf_fd_class, -1);
+  fbuf->fd = fd;
+  return &fbuf->up;
+}
+
+/* Opens FILENAME with FLAGS and MODE and stores a new fbuf that represents it
+   into *FBUFP.  Returns 0 on success, or a positive errno value on failure.
+   ON failure, *FBUFP will be NULL. */
+int
+fbuf_open_file (const char *filename, int flags, mode_t mode,
+                struct fbuf **fbufp)
+{
+  int fd = open (filename, flags, mode);
+  if (fd < 0)
+    {
+      *fbufp = NULL;
+      return errno;
+    }
+  *fbufp = fbuf_open_fd (fd);
+  return 0;
+}
+
+static struct fbuf_fd *
+fbuf_fd_cast (const struct fbuf *fbuf)
+{
+  assert (fbuf->class == &fbuf_fd_class);
+  return UP_CAST (fbuf, struct fbuf_fd, up);
+}
+
+static int
+fbuf_fd_close (struct fbuf *fbuf_)
+{
+  struct fbuf_fd *fbuf = fbuf_fd_cast (fbuf_);
+  int retval = close (fbuf->fd) == EOF ? errno : 0;
+  free (fbuf);
+  return retval;
+}
+
+static int
+fbuf_fd_read (struct fbuf *fbuf_, void *buffer, size_t n)
+{
+  struct fbuf_fd *fbuf = fbuf_fd_cast (fbuf_);
+  int retval = read (fbuf->fd, buffer, n);
+  return retval >= 0 ? retval : -errno;
+}
+
+static int
+fbuf_fd_write (struct fbuf *fbuf_, const void *buffer, size_t n)
+{
+  struct fbuf_fd *fbuf = fbuf_fd_cast (fbuf_);
+  int retval = write (fbuf->fd, buffer, n);
+  return retval > 0 ? retval : -errno;
+}
+
+static int
+fbuf_fd_seek (struct fbuf *fbuf_, off_t offset)
+{
+  struct fbuf_fd *fbuf = fbuf_fd_cast (fbuf_);
+  return lseek (fbuf->fd, offset, SEEK_SET) < 0 ? errno : 0;
+}
+
+static off_t
+fbuf_fd_tell (struct fbuf *fbuf_)
+{
+  struct fbuf_fd *fbuf = fbuf_fd_cast (fbuf_);
+  off_t offset = lseek (fbuf->fd, 0, SEEK_CUR);
+  return offset >= 0 ? offset : -errno;
+}
+
+static off_t
+fbuf_fd_get_size (struct fbuf *fbuf_)
+{
+  struct fbuf_fd *fbuf = fbuf_fd_cast (fbuf_);
+  off_t offset = lseek (fbuf->fd, 0, SEEK_END);
+  return offset >= 0 ? offset : -errno;
+}
+
+static const struct fbuf_class fbuf_fd_class =
+  {
+    fbuf_fd_close,
+    fbuf_fd_read,
+    fbuf_fd_write,
+    fbuf_fd_seek,
+    fbuf_fd_tell,
+    fbuf_fd_get_size,
+  };
+\f
+struct fbuf_memory
+  {
+    struct fbuf up;
+    uint8_t *data;
+    size_t size, allocated;
+  };
+
+static const struct fbuf_class fbuf_memory_class;
+
+/* Takes ownership of the N bytes of data at DATA, which must have been
+   allocated with malloc(), as a memory buffer and makes it the backing for the
+   newly returned fbuf.  Initially, the fbuf is positioned at the beginning of
+   the data, so that reads will read from it and writes will overwrite it.  (To
+   append, use fbuf_seek() to seek to the end.)
+
+   Writes beyond the end will reallocate the buffer.  Closing the returned fbuf
+   will free the buffer. */
+struct fbuf *
+fbuf_open_memory (void *data, size_t n)
+{
+  struct fbuf_memory *fbuf = xmalloc (sizeof *fbuf);
+  fbuf_init (&fbuf->up, &fbuf_memory_class, 0);
+  fbuf->data = data;
+  fbuf->size = n;
+  fbuf->allocated = n;
+  return &fbuf->up;
+}
+
+static struct fbuf_memory *
+fbuf_memory_cast (const struct fbuf *fbuf)
+{
+  assert (fbuf->class == &fbuf_memory_class);
+  return UP_CAST (fbuf, struct fbuf_memory, up);
+}
+
+static int
+fbuf_memory_close (struct fbuf *fbuf_)
+{
+  struct fbuf_memory *fbuf = fbuf_memory_cast (fbuf_);
+  free (fbuf->data);
+  free (fbuf);
+  return 0;
+}
+
+static int
+fbuf_memory_read (struct fbuf *fbuf_, void *buffer, size_t n)
+{
+  struct fbuf_memory *fbuf = fbuf_memory_cast (fbuf_);
+  if (fbuf->up.offset >= fbuf->size)
+    return 0;
+
+  size_t chunk = MIN (n, fbuf->size - fbuf->up.offset);
+  memcpy (buffer, fbuf->data + fbuf->up.offset, chunk);
+  return chunk;
+}
+
+static int
+fbuf_memory_write (struct fbuf *fbuf_, const void *buffer, size_t n)
+{
+  struct fbuf_memory *fbuf = fbuf_memory_cast (fbuf_);
+
+  /* Fail if write would cause the memory block to exceed SIZE_MAX bytes. */
+  size_t end = xsum (fbuf->up.offset, n);
+  if (size_overflow_p (end))
+    return -EFBIG;
+
+  /* Expand fbuf->data if necessary to hold the write. */
+  if (end > fbuf->allocated)
+    {
+      fbuf->allocated = end < SIZE_MAX / 2 ? end * 2 : end;
+      fbuf->data = xrealloc (fbuf->data, fbuf->allocated);
+    }
+
+  /* Zero-pad to reach the current offset (although this is necessary only if
+     there has been a seek past the end), then copy in the new data. */
+  if (fbuf->up.offset > fbuf->size)
+    memset (fbuf->data + fbuf->size, 0, fbuf->up.offset - fbuf->size);
+  memcpy (fbuf->data + fbuf->up.offset, buffer, n);
+
+  if (end > fbuf->size)
+    fbuf->size = end;
+
+  return n;
+}
+
+static int
+fbuf_memory_seek (struct fbuf *fbuf UNUSED, off_t offset UNUSED)
+{
+  return 0;
+}
+
+static off_t
+fbuf_memory_tell (struct fbuf *fbuf UNUSED)
+{
+  NOT_REACHED ();
+}
+
+static off_t
+fbuf_memory_get_size (struct fbuf *fbuf_)
+{
+  struct fbuf_memory *fbuf = fbuf_memory_cast (fbuf_);
+  return fbuf->size;
+}
+
+static const struct fbuf_class fbuf_memory_class =
+  {
+    fbuf_memory_close,
+    fbuf_memory_read,
+    fbuf_memory_write,
+    fbuf_memory_seek,
+    fbuf_memory_tell,
+    fbuf_memory_get_size,
+  };