/*	$OpenBSD: open_wmemstream.c,v 1.4 2014/10/08 05:28:19 deraadt Exp $	*/

/*
 * Copyright (c) 2011 Martin Pieuchot <mpi@openbsd.org>
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

#include <sys/param.h>

#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <wchar.h>
#include "local.h"

struct state {
	wchar_t		 *string;	/* actual stream */
	wchar_t		**pbuf;		/* point to the stream */
	size_t		 *psize;	/* point to min(pos, len) */
	size_t		  pos;		/* current position */
	size_t		  size;		/* number of allocated wchar_t */
	size_t		  len;		/* length of the data */
	mbstate_t	  mbs;		/* conversion state of the stream */
};

static int
wmemstream_write(void *v, const char *b, int l)
{
	struct state	*st = v;
	wchar_t		*p;
	size_t		 nmc, len, end;

	end = (st->pos + l);

	if (end >= st->size) {
		/* 1.6 is (very) close to the golden ratio. */
		size_t	sz = st->size * 8 / 5;

		if (sz < end + 1)
			sz = end + 1;
		p = reallocarray(st->string, sz, sizeof(wchar_t));
		if (!p)
			return (-1);
		bzero(p + st->size, (sz - st->size) * sizeof(wchar_t));
		*st->pbuf = st->string = p;
		st->size = sz;
	}

	nmc = (st->size - st->pos) * sizeof(wchar_t);
	len = mbsnrtowcs(st->string + st->pos, &b, nmc, l, &st->mbs);
	if (len == (size_t)-1)
		return (-1);
	st->pos += len;

	if (st->pos > st->len) {
		st->len = st->pos;
		st->string[st->len] = L'\0';
	}

	*st->psize = st->pos;

	return (len);
}

static fpos_t
wmemstream_seek(void *v, fpos_t off, int whence)
{
	struct state	*st = v;
	ssize_t		 base = 0;

	switch (whence) {
	case SEEK_SET:
		break;
	case SEEK_CUR:
		base = st->pos;
		break;
	case SEEK_END:
		base = st->len;
		break;
	}

	if (off > (SIZE_MAX / sizeof(wchar_t)) - base || off < -base) {
		errno = EOVERFLOW;
		return (-1);
	}

	/*
	 * XXX Clearing mbs here invalidates shift state for state-
	 * dependent encodings, but they are not (yet) supported.
	 */
	bzero(&st->mbs, sizeof(st->mbs));

	st->pos = base + off;
	*st->psize = MIN(st->pos, st->len);

	return (st->pos);
}

static int
wmemstream_close(void *v)
{
	struct state	*st = v;

	free(st);

	return (0);
}

FILE *
open_wmemstream(wchar_t **pbuf, size_t *psize)
{
	struct state	*st;
	FILE		*fp;

	if (pbuf == NULL || psize == NULL) {
		errno = EINVAL;
		return (NULL);
	}

	if ((st = malloc(sizeof(*st))) == NULL)
		return (NULL);

	if ((fp = __sfp()) == NULL) {
		free(st);
		return (NULL);
	}

	st->size = BUFSIZ * sizeof(wchar_t);
	if ((st->string = calloc(1, st->size)) == NULL) {
		free(st);
		fp->_flags = 0;
		return (NULL);
	}

	*st->string = L'\0';
	st->pos = 0;
	st->len = 0;
	st->pbuf = pbuf;
	st->psize = psize;
	bzero(&st->mbs, sizeof(st->mbs));

	*pbuf = st->string;
	*psize = st->len;

	fp->_flags = __SWR;
	fp->_file = -1;
	fp->_cookie = st;
	fp->_read = NULL;
	fp->_write = wmemstream_write;
	fp->_seek = wmemstream_seek;
	fp->_close = wmemstream_close;
	_SET_ORIENTATION(fp, 1);

	return (fp);
}
