root/usr/src/lib/libc/port/stdio/open_memstream.c
/*
 * 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 open_memstream(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 struct memstream {
        char *mstr_buf;
        size_t mstr_alloc;
        size_t mstr_pos;
        size_t mstr_lsize;
        char **mstr_ubufp;
        size_t *mstr_usizep;
} memstream_t;

/*
 * Common seek and overflow detection logic for the memory stream family of
 * functions (open_memstream, open_wmemstream, etc.). We need to validate
 * several things:
 *
 *  - That the offset when applied to base doesn't cause an over or underflow.
 *  - That the resulting offset is positive (done implicitly with the above)
 *  - That the resulting offset does not exceed an off_t's maximum size.
 *    Unfortunately the kernel doesn't export an OFF_MAX value to userland, so
 *    we have to know that it will always be equivalent to the environment's
 *    long. This is designed with the assumption that in an ILP32 environment we
 *    care about an off_t and not an off64_t. In cases where an off64_t is
 *    valid, we still have to fit inside of the size_t constraints.
 *
 * We check for each of the cases and only perform unsigned arithmetic to verify
 * that we have defined behavior.
 */
boolean_t
memstream_seek(size_t base, off_t off, size_t max, size_t *nposp)
{
        size_t npos;

        npos = base + (size_t)off;
        if (off >= 0 && npos < base) {
                return (B_FALSE);
        }

        if (off >= 0 && npos > LONG_MAX) {
                return (B_FALSE);
        }

        if (off < 0 && npos >= base) {
                return (B_FALSE);
        }

        if (npos > max) {
                return (B_FALSE);
        }

        *nposp = npos;
        return (B_TRUE);
}

int
memstream_newsize(size_t pos, size_t alloc, size_t nbytes, size_t *nallocp)
{
        size_t npos = pos + nbytes + 1;
        if (npos < pos) {
                /*
                 * We've been asked to write a number of bytes that would result
                 * in an overflow in the position. This means the stream would
                 * need to allocate all of memory, that's impractical.
                 */
                errno = EOVERFLOW;
                return (-1);
        }

        /*
         * If the new position is beyond the allocated amount, grow the array to
         * a practical amount.
         */
        if (npos > alloc) {
                size_t newalloc = P2ROUNDUP(npos, BUFSIZ);
                if (newalloc < npos) {
                        errno = EOVERFLOW;
                        return (-1);
                }
                *nallocp = newalloc;
                return (1);
        }

        return (0);
}

/*
 * The SUSv4 spec says that this should not support reads.
 */
static ssize_t
open_memstream_read(FILE *iop __unused, char *buf __unused,
    size_t nbytes __unused)
{
        errno = EBADF;
        return (-1);
}

static ssize_t
open_memstream_write(FILE *iop, const char *buf, size_t nbytes)
{
        memstream_t *memp = _xdata(iop);
        size_t newsize;
        int ret;

        /*
         * We need to fit inside of an ssize_t, so we need to first constrain
         * nbytes to a reasonable value.
         */
        nbytes = MIN(nbytes, SSIZE_MAX);
        ret = memstream_newsize(memp->mstr_pos, memp->mstr_alloc, nbytes,
            &newsize);
        if (ret < 0) {
                return (-1);
        } else if (ret > 0) {
                void *temp;
                temp = recallocarray(memp->mstr_buf, memp->mstr_alloc,
                    newsize, sizeof (char));
                if (temp == NULL) {
                        return (-1);
                }
                memp->mstr_buf = temp;
                memp->mstr_alloc = newsize;
                *memp->mstr_ubufp = temp;
        }

        (void) memcpy(&memp->mstr_buf[memp->mstr_pos], buf, nbytes);
        memp->mstr_pos += nbytes;

        if (memp->mstr_pos > memp->mstr_lsize) {
                memp->mstr_lsize = memp->mstr_pos;
                memp->mstr_buf[memp->mstr_pos] = '\0';
        }
        *memp->mstr_usizep = MIN(memp->mstr_pos, memp->mstr_lsize);

        return (nbytes);
}

static off_t
open_memstream_seek(FILE *iop, off_t off, int whence)
{
        memstream_t *memp = _xdata(iop);
        size_t base, npos;

        switch (whence) {
        case SEEK_SET:
                base = 0;
                break;
        case SEEK_CUR:
                base = memp->mstr_pos;
                break;
        case SEEK_END:
                base = memp->mstr_lsize;
                break;
        default:
                errno = EINVAL;
                return (-1);
        }

        if (!memstream_seek(base, off, SSIZE_MAX, &npos)) {
                errno = EINVAL;
                return (-1);
        }
        memp->mstr_pos = npos;
        *memp->mstr_usizep = MIN(memp->mstr_pos, memp->mstr_lsize);

        return ((off_t)memp->mstr_pos);
}

static int
open_memstream_close(FILE *iop)
{
        memstream_t *memp = _xdata(iop);
        free(memp);
        _xunassoc(iop);
        return (0);
}

FILE *
open_memstream(char **bufp, size_t *sizep)
{
        FILE *iop;
        memstream_t *memp;

        if (bufp == NULL || sizep == NULL) {
                errno = EINVAL;
                return (NULL);
        }

        memp = calloc(1, sizeof (memstream_t));
        if (memp == NULL) {
                return (NULL);
        }

        memp->mstr_alloc = BUFSIZ;
        memp->mstr_buf = calloc(memp->mstr_alloc, sizeof (char));
        if (memp->mstr_buf == NULL) {
                goto cleanup;
        }
        memp->mstr_buf[0] = '\0';
        memp->mstr_pos = 0;
        memp->mstr_lsize = 0;
        memp->mstr_ubufp = bufp;
        memp->mstr_usizep = sizep;

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

#ifdef  _LP64
        iop->_flag = (iop->_flag & ~_DEF_FLAG_MASK) | _IOWRT;
#else
        iop->_flag = _IOWRT;
#endif

        /*
         * Update the user pointers now, in case a call to fflush() happens
         * immediately.
         */

        if (_xassoc(iop, open_memstream_read, open_memstream_write,
            open_memstream_seek, open_memstream_close, memp) != 0) {
                goto cleanup;
        }
        _setorientation(iop, _BYTE_MODE);
        SET_SEEKABLE(iop);

        *memp->mstr_ubufp = memp->mstr_buf;
        *memp->mstr_usizep = MIN(memp->mstr_pos, memp->mstr_lsize);

        return (iop);

cleanup:
        free(memp->mstr_buf);
        free(memp);
        return (NULL);
}