/*
 * This file and its contents are supplied under the terms of the
 * Common Development and Distribution License ("CDDL"), version 1.0.
 * You may only use this file in accordance with the terms of version
 * 1.0 of the CDDL.
 *
 * A full copy of the text of the CDDL should have accompanied this
 * source.  A copy of the CDDL is also available via the Internet at
 * http://www.illumos.org/license/CDDL.
 */

/*
 * Copyright 2020 Robert Mustacchi
 */

/*
 * Implements fmemopen(3C).
 */

#include "mtlib.h"
#include "file64.h"
#include <stdio.h>
#include "stdiom.h"
#include <errno.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/sysmacros.h>
#include <limits.h>

typedef enum fmemopen_flags {
	/*
	 * Indicates that the user gave us the buffer and so we shouldn't free
	 * it.
	 */
	FMO_F_USER_BUFFER	= 1 << 0,
	/*
	 * When the stream is open for update (a, a+) then we have to have
	 * slightly different behavior on write and zeroing the buffer.
	 */
	FMO_F_APPEND		= 1 << 1
} fmemopen_flags_t;

typedef struct fmemopen {
	/*
	 * Pointer to the underlying memory stream.
	 */
	char	*fmo_buf;
	/*
	 * Allocated length of the buffer.
	 */
	size_t	fmo_alloc;
	/*
	 * Current position of the buffer.
	 */
	size_t	fmo_pos;
	/*
	 * Current 'size' of the buffer. POSIX describes a size that the buffer
	 * has which is separate from the allocated size, but cannot exceed it.
	 */
	size_t	fmo_lsize;
	fmemopen_flags_t fmo_flags;
} fmemopen_t;

static ssize_t
fmemopen_read(FILE *iop, char *buf, size_t nbytes)
{
	fmemopen_t *fmp = _xdata(iop);

	nbytes = MIN(nbytes, fmp->fmo_lsize - fmp->fmo_pos);
	if (nbytes == 0) {
		return (0);
	}

	(void) memcpy(buf, fmp->fmo_buf, nbytes);
	fmp->fmo_pos += nbytes;

	return (nbytes);
}

static ssize_t
fmemopen_write(FILE *iop, const char *buf, size_t nbytes)
{
	size_t npos;
	fmemopen_t *fmp = _xdata(iop);

	if ((fmp->fmo_flags & FMO_F_APPEND) != 0) {
		/*
		 * POSIX says that if append mode is in effect, we must always
		 * seek to the logical size. This effectively is mimicking the
		 * O_APPEND behavior.
		 */
		fmp->fmo_pos = fmp->fmo_lsize;
	}

	if (nbytes == 0) {
		return (0);
	} else if (nbytes >= SSIZE_MAX) {
		errno = EINVAL;
		return (-1);
	}

	npos = fmp->fmo_pos + nbytes;
	if (npos < nbytes) {
		errno = EOVERFLOW;
		return (-1);
	} else if (npos > fmp->fmo_alloc) {
		nbytes = fmp->fmo_alloc - fmp->fmo_pos;
	}

	(void) memcpy(&fmp->fmo_buf[fmp->fmo_pos], buf, nbytes);
	fmp->fmo_pos += nbytes;

	if (fmp->fmo_pos > fmp->fmo_lsize) {
		fmp->fmo_lsize = fmp->fmo_pos;

		/*
		 * POSIX distinguishes behavior for writing a NUL in these
		 * streams. Basically if we are open for update and we are at
		 * the end of the buffer, we don't place a NUL. Otherwise, we
		 * always place one at the current position (or the end if we
		 * were over the edge).
		 */
		if (fmp->fmo_lsize < fmp->fmo_alloc) {
			fmp->fmo_buf[fmp->fmo_lsize] = '\0';
		} else if ((fmp->fmo_flags & FMO_F_APPEND) == 0) {
			fmp->fmo_buf[fmp->fmo_alloc - 1] = '\0';
		}
	}

	return (nbytes);
}

static off_t
fmemopen_seek(FILE *iop, off_t off, int whence)
{
	fmemopen_t *fmp = _xdata(iop);
	size_t base, npos;

	switch (whence) {
	case SEEK_SET:
		base = 0;
		break;
	case SEEK_CUR:
		base = fmp->fmo_pos;
		break;
	case SEEK_END:
		base = fmp->fmo_lsize;
		break;
	default:
		errno = EINVAL;
		return (-1);
	}

	if (!memstream_seek(base, off, fmp->fmo_alloc, &npos)) {
		errno = EINVAL;
		return (-1);
	}
	fmp->fmo_pos = npos;

	return ((off_t)fmp->fmo_pos);
}

static void
fmemopen_free(fmemopen_t *fmp)
{
	if (fmp->fmo_buf != NULL &&
	    (fmp->fmo_flags & FMO_F_USER_BUFFER) == 0) {
		free(fmp->fmo_buf);
	}

	free(fmp);
}

static int
fmemopen_close(FILE *iop)
{
	fmemopen_t *fmp = _xdata(iop);
	fmemopen_free(fmp);
	_xunassoc(iop);
	return (0);
}

FILE *
fmemopen(void *_RESTRICT_KYWD buf, size_t size,
    const char *_RESTRICT_KYWD mode)
{
	int oflags, fflags, err;
	fmemopen_t *fmp;
	FILE *iop;

	if (size == 0 || mode == NULL) {
		errno = EINVAL;
		return (NULL);
	}

	if (_stdio_flags(mode, &oflags, &fflags) != 0) {
		/* errno set for us */
		return (NULL);
	}

	/*
	 * buf is only allowed to be NULL if the '+' is specified.  If the '+'
	 * mode was specified, then we'll have fflags set to _IORW.
	 */
	if (buf == NULL && fflags != _IORW) {
		errno = EINVAL;
		return (NULL);
	}

	if ((fmp = calloc(1, sizeof (fmemopen_t))) == NULL) {
		errno = ENOMEM;
		return (NULL);
	}

	if (buf == NULL) {
		fmp->fmo_buf = calloc(size, sizeof (uint8_t));
		if (fmp->fmo_buf == NULL) {
			errno = ENOMEM;
			goto cleanup;
		}
	} else {
		fmp->fmo_buf = buf;
		fmp->fmo_flags |= FMO_F_USER_BUFFER;
	}

	fmp->fmo_alloc = size;

	/*
	 * Set the initial logical size and position depending on whether we're
	 * using r(+), w(+), and a(+). The latter two are identified by O_TRUNC
	 * and O_APPEND in oflags.
	 */
	if ((oflags & O_APPEND) != 0) {
		fmp->fmo_pos = strnlen(fmp->fmo_buf, fmp->fmo_alloc);
		fmp->fmo_lsize = fmp->fmo_pos;
		fmp->fmo_flags |= FMO_F_APPEND;
	} else if ((oflags & O_TRUNC) != 0) {
		fmp->fmo_buf[0] = '\0';
		fmp->fmo_pos = 0;
		fmp->fmo_lsize = 0;
	} else {
		fmp->fmo_pos = 0;
		fmp->fmo_lsize = size;
	}

	iop = _findiop();
	if (iop == NULL) {
		goto cleanup;
	}

#ifdef	_LP64
	iop->_flag = (iop->_flag & ~_DEF_FLAG_MASK) | fflags;
#else
	iop->_flag = fflags;
#endif
	if (_xassoc(iop, fmemopen_read, fmemopen_write, fmemopen_seek,
	    fmemopen_close, fmp) != 0) {
		goto cleanup;
	}

	SET_SEEKABLE(iop);

	return (iop);

cleanup:
	err = errno;
	fmemopen_free(fmp);
	errno = err;
	return (NULL);
}