+/* 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,
+ };