Subject: Re: SUSv3 extended API set
To: None <pavel.cahyna@st.mff.cuni.cz>
From: Bruce Korb <Bruce.Korb@gmail.com>
List: tech-userlevel
Date: 12/18/2005 07:36:34
This is a multi-part message in MIME format.
--------------040105040206010301050101
Content-Type: text/plain; charset=ISO-8859-1; format=flowed
Content-Transfer-Encoding: 7bit

> Pavel Cahyna wrote:
> On Sat, Dec 17, 2005 at 12:03:00AM +0000, Rui Paulo wrote:
>
>> I just saw this on the freebsd-standards mailing list.
>> http://lists.freebsd.org/pipermail/freebsd-standards/2005-December/001087.html
>>
>> We already support some of them, but I guess we should start working
>> to implement the new ones.
>>
>> Thoughts?
>
>
> I have an implementation of fmemopen(). If you are interested in it, it's
> attached.
>
> Bye Pavel

Attached is an implementation that handles 'b' in the mode string.
It is a _slight_ extension to the POSIX interface in that:

1. if 'b' is not in the mode and there is space in the buffer, a NUL
   byte is written after the last output byte

2. The buffer can be expanded, if the caller does not supply the
   buffer.  (i.e., if BUF is NULL, then it is extensible and allocated
   by fmemopen code.

Also, the code accommodates glibc's fopen_cookie as well as funopen.
(You don't care, but I do. :)

It is based on an old version written by Hanno Mueller, kontakt@hanno.de

Cheers - Bruce


--------------040105040206010301050101
Content-Type: text/x-csrc;
 name="fmemopen.c"
Content-Transfer-Encoding: 7bit
Content-Disposition: inline;
 filename="fmemopen.c"

/*-
 * Copyright (c) 2004-2005
 *  Bruce Korb.  All rights reserved.
 *
 * Time-stamp:      "2005-12-18 07:29:01 bkorb"
 *
 * This code was inspired from software written by
 *   Hanno Mueller, kontakt@hanno.de
 * and completely rewritten by Bruce Korb, bkorb@gnu.org
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. Neither the name of the author nor the name of any other contributor
 *    may be used to endorse or promote products derived from this software
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 */
#if defined(ENABLE_FMEMOPEN)

#define AO_FMEM_HASH(_p)  ((((uintptr_t)_p) >> 4) & 0x07)

/**--subblock=arg=arg_type,arg_name,arg_desc =*/
/***
 * library:  fmem
 * header:   libfmem.h
 *
 * lib_description:
 *
 *  This library only functions in the presence of GNU or BSD's libc.
 *  It is distributed under the Berkeley Software Distribution License.
 *  This library can only function if there is a libc-supplied mechanism
 *  for fread/fwrite/fseek/fclose to call into this code.  GNU uses
 *  fopencookie and BSD supplies funopen.
 **/
/*
 * ag_fmemopen() - "my" version of a string stream
 * inspired by the glibc version, but completely rewritten and
 * extended by Bruce Korb - bkorb@gnu.org
 *
 * - See the man page.
 */

#if defined(__linux) && ! defined(_GNU_SOURCE)
#  define _GNU_SOURCE
#endif

#include <sys/types.h>
#include <fcntl.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#if defined(HAVE_FOPENCOOKIE)
#  include <libio.h>
   typedef _IO_off64_t  fmem_off_t;
   typedef int          seek_pos_t;

#elif defined(HAVE_FUNOPEN)
   typedef size_t  fmem_off_t;
   typedef fpos_t  seek_pos_t;

   typedef int     (cookie_read_function_t )(void *, char *, int);
   typedef int     (cookie_write_function_t)(void *, const char *, int);
   typedef fpos_t  (cookie_seek_function_t )(void *, fpos_t, int);
   typedef int     (cookie_close_function_t)(void *);

#else
#  error  OOPS
#endif

#define PROP_TABLE \
_Prop_( read,       "Read from buffer"        ) \
_Prop_( write,      "Write to buffer"         ) \
_Prop_( append,     "Append to buffer okay"   ) \
_Prop_( binary,     "byte data - not string"  ) \
_Prop_( create,     "allocate the string"     ) \
_Prop_( truncate,   "start writing at start"  ) \
_Prop_( allocated,  "we allocated the buffer" ) \
_Prop_( fixed_size, "writes do not append"    )

#ifndef NUL
# define NUL '\0'
#endif

#define _Prop_(n,s)   BIT_ID_ ## n,
typedef enum { PROP_TABLE BIT_CT } fmem_flags_e;
#undef  _Prop_

#define FLAG_BIT(n)  (1 << BIT_ID_ ## n)

typedef unsigned long mode_bits_t;
typedef unsigned char buf_bytes_t;

typedef struct fmem_cookie_s fmem_cookie_t;
struct fmem_cookie_s {
    FILE*          fp;
    fmem_cookie_t *next;        /* linked list of these */
    buf_bytes_t   *buffer;
    size_t         buf_size;    /* Full size of buffer */
    size_t         next_ix;     /* Current position */
    size_t         eof;         /* End Of File */
    size_t         pg_size;     /* size of a memory page.
                                   Future architectures allow it to vary
                                   by memory region. */
    mode_bits_t    mode;
};

static fmem_cookie_t*  cookie_list[8] = {
    NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL };

/* = = = START-STATIC-FORWARD = = = */
/* static forward declarations maintained by :mkfwd */
static int
fmem_getmode( const char *pMode, mode_bits_t *pRes );

static int
fmem_extend( fmem_cookie_t *pFMC, size_t new_size );

static ssize_t
fmem_read( void *cookie, void *pBuf, size_t sz );

static ssize_t
fmem_write( void *cookie, const void *pBuf, size_t sz );

static seek_pos_t
fmem_seek (void *cookie, fmem_off_t *p_offset, int dir);

static int
fmem_close( void *cookie );
/* = = = END-STATIC-FORWARD = = = */

static int
fmem_getmode( const char *pMode, mode_bits_t *pRes )
{
    if (pMode == NULL)
        return 1;

    switch (*pMode) {
    case 'a': *pRes = FLAG_BIT(write) | FLAG_BIT(append);
              break;
    case 'w': *pRes = FLAG_BIT(write) | FLAG_BIT(truncate);
              break;
    case 'r': *pRes = FLAG_BIT(read);
              break;
    default:  return EINVAL;
    }

    /*
     *  If someone wants to supply a "wxxbxbbxbb+" pMode string, I don't care.
     */
    for (;;) {
        switch (*++pMode) {
        case '+': *pRes |= FLAG_BIT(read) | FLAG_BIT(write);
                  if (pMode[1] != NUL)
                      return EINVAL;
                  break;
        case NUL: break;
        case 'b': *pRes |= FLAG_BIT(binary); continue;
        case 'x': continue;
        default:  return EINVAL;
        }
        break;
    }

    return 0;
}

static int
fmem_extend( fmem_cookie_t *pFMC, size_t new_size )
{
    size_t ns = (new_size + (pFMC->pg_size - 1)) & (~(pFMC->pg_size - 1));

    /*
     *  We can expand the buffer only if we are in append mode.
     */
    if (pFMC->mode & FLAG_BIT(fixed_size))
        goto no_space;

    if ((pFMC->mode & FLAG_BIT(allocated)) == 0) {
        /*
         *  Previously, this was a user supplied buffer.  We now move to one
         *  of our own.  The user is responsible for the earlier memory.
         */
        void* bf = malloc( ns );
        if (bf == NULL)
            goto no_space;

        memcpy( bf, pFMC->buffer, pFMC->buf_size );
        pFMC->buffer = bf;
        pFMC->mode  |= FLAG_BIT(allocated);
    }
    else {
        void* bf = realloc( pFMC->buffer, ns );
        if (bf == NULL)
            goto no_space;

        pFMC->buffer = bf;
    }

    /*
     *  Unallocated file space is set to zeros.  Emulate that.
     */
    memset( pFMC->buffer + pFMC->buf_size, 0, ns - pFMC->buf_size );
    pFMC->buf_size = ns;
    return 0;

 no_space:
    errno = ENOSPC;
    return -1;
}

static ssize_t
fmem_read( void *cookie, void *pBuf, size_t sz )
{
    fmem_cookie_t *pFMC = cookie;

    if (pFMC->next_ix + sz > pFMC->eof) {
        if (pFMC->next_ix >= pFMC->eof)
            return (sz > 0) ? -1 : 0;
        sz = pFMC->eof - pFMC->next_ix;
    }

    memcpy(pBuf, pFMC->buffer + pFMC->next_ix, sz);

    pFMC->next_ix += sz;

    return sz;
}


static ssize_t
fmem_write( void *cookie, const void *pBuf, size_t sz )
{
    fmem_cookie_t *pFMC = cookie;
    int add_nul_char;

    /*
     *  In append mode, always seek to the end before writing.
     */
    if (pFMC->mode & FLAG_BIT(append))
        pFMC->next_ix = pFMC->eof;

    /*
     *  Only add a NUL character if:
     *
     *  * we are not in binary mode
     *  * there are data to write
     *  * the last character to write is not already NUL
     */
    add_nul_char =
           ((pFMC->mode & FLAG_BIT(binary)) != 0)
        && (sz > 0)
        && (((char*)pBuf)[sz - 1] != NUL);

    {
        size_t next_pos = pFMC->next_ix + sz + add_nul_char;
        if (next_pos > pFMC->buf_size) {
            if (fmem_extend( pFMC, next_pos ) != 0) {
                /*
                 *  We could not extend the memory.  Try to write some data.
                 *  Fail if we are either at the end or not writing data.
                 */
                if ((pFMC->next_ix >= pFMC->buf_size) || (sz == 0))
                    return -1; /* no space at all.  errno is set. */

                /*
                 *  Never add the NUL for a truncated write.  "sz" may be
                 *  unchanged or limited here.
                 */
                add_nul_char = 0;
                sz = pFMC->buf_size - pFMC->next_ix;
            }
        }
    }

    memcpy( pFMC->buffer + pFMC->next_ix, pBuf, sz);

    pFMC->next_ix += sz;

    /*
     *  Check for new high water mark and remember it.  Add a NUL if
     *  we do that and if we have a new high water mark.
     */
    if (pFMC->next_ix > pFMC->eof) {
        pFMC->eof = pFMC->next_ix;
        if (add_nul_char)
            /*
             *  There is space for this NUL.  The "add_nul_char" is not part of
             *  the "sz" that was added to "next_ix".
             */
            pFMC->buffer[ pFMC->eof ] = NUL;
    }

    return sz;
}


static seek_pos_t
fmem_seek (void *cookie, fmem_off_t *p_offset, int dir)
{
    fmem_off_t new_pos;
    fmem_cookie_t *pFMC = cookie;

    switch (dir) {
    case SEEK_SET: new_pos = *p_offset;  break;
    case SEEK_CUR: new_pos = pFMC->next_ix  + *p_offset;  break;
    case SEEK_END: new_pos = pFMC->eof - *p_offset;  break;

    default:
        goto seek_oops;
    }

    if ((signed)new_pos < 0)
        goto seek_oops;

    if (new_pos > pFMC->buf_size) {
        if (fmem_extend( pFMC, new_pos ))
            return -1; /* errno is set */
    }

    pFMC->next_ix = new_pos;
    return new_pos;

 seek_oops:
    errno = EINVAL;
    return -1;
}


static int
fmem_close( void *cookie )
{
    fmem_cookie_t *pFMC = cookie;
    fmem_cookie_t**ppFMC= cookie_list + AO_FMEM_HASH(pFMC->fp);

    while (*ppFMC != pFMC) {
        if (*ppFMC == NULL)
            return EINVAL;
        ppFMC = &( (*ppFMC)->next );
    }

    *ppFMC = (*ppFMC)->next;

    if (pFMC->mode & FLAG_BIT(allocated))
        free( pFMC->buffer );
    free( pFMC );

    return 0;
}


/*=export_func ao_fioctl
 * private:
 *
 *  what:  get information about a string stream
 *
 *  arg: + FILE* + fptr  + the string stream
 *  arg: + int   + req   + the requested data
 *  arg: + void* + ptr   + ptr to result area
 *
 *  ret-type:  int
 *  ret-desc:  zero on success
 *
 *  err: non-zero is returned and @code{errno} is set to @code{EINVAL}.
 *
 *  doc:
 *
 *  This routine surreptitiously slips in a special request.
 *  The commands supported are:
 *
 *  @table @code
 *  @item FMEM_IOCTL_BUF_ADDR
 *
 *    Retrieve the address of the buffer.  Future output to the stream might
 *    cause this buffer to be freed and the contents copied to another buffer.
 *    You must ensure that either you have saved the buffer (see
 *    @code{FMEM_IOCTL_SAVE_BUF} below), or do not do any more I/O to it while
 *    you are using this address.
 *
 *    "ptr" must point to a @code{char*} pointer.
 *
 *  @item FMEM_IOCTL_SAVE_BUF
 *
 *    Do not deallocate the buffer on close.  You would likely want to use
 *    this after writing all the output data and just before closing.
 *    Otherwise, the buffer might get relocated.  Once you have specified
 *    this, the current buffer becomes the client program's resposibility to
 *    @code{free()}.  If more I/O operations are performed, a new buffer
 *    @i{may} get allocated.  @code{fmem_close} will free that new buffer
 *    and the user will remain responsible for @code{free()}-ing this buffer.
 *
 *    "ptr" must point to a @code{char*} pointer.
 *
 *  @end table
 *
 *  The third argument is never optional and must be a pointer to where data
 *  are to be retrieved or stored.  It may be NULL if there are no data to
 *  transfer, but both of these functions currently return the address of the
 *  buffer.  This is implemented as a wrapper around @code{fseek(3C)}, so
 *  the "req" argument must not conflict with @code{SEEK_SET}, @code{SEEK_CUR}
 *  or @code{SEEK_END}.
=*/
int
ao_fioctl( FILE* fp, int req, void* ptr )
{
    fmem_cookie_t* pC = cookie_list[ AO_FMEM_HASH(fp) ];

    for (;;) {
        if (pC == NULL)
            return ioctl( fileno(fp), req, ptr );

        if (pC->fp == fp)
            break;

        pC = pC->next;
    }

    switch (req) {
    case FMEM_IOCTL_SAVE_BUF:
        pC->mode &= ~FLAG_BIT(allocated);
        /* FALLTHROUGH */

    case FMEM_IOCTL_BUF_ADDR:
    {
        char** ppz = ptr;
        *ppz = (char*)(pC->buffer);
    }

    default:
        errno = EINVAL;
        return -1;
    }

    return 0;
}


/*=export_func ao_fmemopen
 * private:
 *
 *  what:  Open a stream to a string
 *
 *  arg: + void*       + buf  + buffer to use for i/o +
 *  arg: + ssize_t     + len  + size of the buffer +
 *  arg: + const char* + mode + mode string, a la fopen(3C) +
 *
 *  ret-type:  FILE*
 *  ret-desc:  a stdio FILE* pointer
 *
 *  err:  NULL is returned and errno is set to @code{EINVAL} or @code{ENOSPC}.
 *
 *  doc:
 *
 *  This function requires underlying @var{libc} functionality:
 *  either @code{fopencookie(3GNU)} or @code{funopen(3BSD)}.
 *
 *  If @var{buf} is @code{NULL}, then a buffer is allocated.
 *  The initial allocation is @var{len} bytes.  If @var{len} is
 *  less than zero, then the buffer will not be reallocated if more
 *  space is needed.  Any allocated memory is @code{free()}-ed
 *  when @code{fclose(3C)} is called.
 *
 *  If @code{buf} is not @code{NULL}, then @code{len} must not be zero.
 *  It may still be less than zero to indicate that the buffer is not to
 *  be reallocated.
 *
 *  The mode string is interpreted as follows.  If the first character of
 *  the mode is:
 *
 *  @table @code
 *  @item a
 *  Then the string is opened in "append" mode.  In binary mode, "appending"
 *  will begin from the end of the initial buffer.  Otherwise, appending will
 *  start at the first NUL character in the initial buffer (or the end of the
 *  buffer if there is no NUL character).  Do not use fixed size buffers
 *  (negative @var{len} lengths) in append mode.
 *
 *  @item w
 *  Then the string is opened in "write" mode.  Any initial buffer is presumed
 *  to be empty.
 *
 *  @item r
 *  Then the string is opened in "read" mode.
 *  @end table
 *
 *  @noindent
 *  If it is not one of these three, the open fails and @code{errno} is
 *  set to @code{EINVAL}.  These initial characters may be followed by:
 *
 *  @table @code
 *  @item +
 *  The buffer is marked as updatable and both reading and writing is enabled.
 *
 *  @item b
 *  The I/O is marked as "binary" and a trailing NUL will not be inserted into
 *  the buffer.  Without this mode flag, one will be inserted after the
 *  @code{EOF}, if it fits.  It will fit if the buffer is extensible (the
 *  provided @var{len} was not negative).  This mode flag has no effect if
 *  the buffer is opened in read-only mode.
 *
 *  @item x
 *  This is ignored.
 *  @end table
 *
 *  @noindent
 *  Any other letters following the inital 'a', 'w' or 'r' will cause an error.
=*/
FILE *
ao_fmemopen(void *buf, ssize_t len, const char *pMode)
{
    fmem_cookie_t *pFMC;

    {
        mode_bits_t mode;

        if (fmem_getmode(pMode, &mode) != 0) {
            return NULL;
        }

        pFMC = malloc(sizeof(fmem_cookie_t));
        if (pFMC == NULL) {
            errno = ENOMEM;
            return NULL;
        }

        pFMC->mode = mode;
    }

    pFMC->pg_size = getpagesize();

    if (buf != NULL) {
        /*
         *  User allocated buffer.  User responsible for disposal.
         */
        if (len == 0) {
            free( pFMC );
            errno = EINVAL;
            return NULL;
        }

        pFMC->mode |= FLAG_BIT(fixed_size);
        /*  Figure out where our "next byte" and EOF are.
         *  Truncated files start at the beginning.
         */
        if (pFMC->mode & FLAG_BIT(truncate)) {
            /*
             *  "write" mode
             */
            pFMC->eof = \
            pFMC->next_ix = 0;
        }

        else if (pFMC->mode & FLAG_BIT(binary)) {
            pFMC->eof = len;
            pFMC->next_ix    = (pFMC->mode & FLAG_BIT(append)) ? len : 0;

        } else {
            /*
             * append or read text mode -- find the end of the buffer
             * (the first NUL character)
             */
            buf_bytes_t *p = pFMC->buffer = (buf_bytes_t*)buf;
            pFMC->eof = 0;
            while ((*p != NUL) && (++(pFMC->eof) < len))  p++;
            pFMC->next_ix =
                (pFMC->mode & FLAG_BIT(append)) ? pFMC->eof : 0;
        }

        /*
         *  text mode - NUL terminate buffer, if it fits.
         */
        if (  ((pFMC->mode & FLAG_BIT(binary)) == 0)
           && (pFMC->next_ix < len)) {
            pFMC->buffer[pFMC->next_ix] = NUL;
        }
    }

    else if ((pFMC->mode & (FLAG_BIT(append) | FLAG_BIT(truncate))) == 0) {
        /*
         *  Not appending and not truncating.  We must be reading.
         *  We also have no user supplied buffer.  Nonsense.
         */
        errno = EINVAL;
        free( pFMC );
        return NULL;
    }

    else {
        /*
         *  We must allocate the buffer.  If "len" is zero, set it to page size.
         */
        pFMC->mode |= FLAG_BIT(allocated);
        if (len == 0)
            len = pFMC->pg_size;

        /*
         *  Unallocated file space is set to NULs.  Emulate that.
         */
        pFMC->buffer = calloc(1, len);
        if (pFMC->buffer == NULL) {
            errno = ENOMEM;
            free( pFMC );
            return NULL;
        }

        /*
         *  We've allocated the buffer.  The end of file and next entry
         *  are both zero.
         */
        pFMC->next_ix = 0;
        pFMC->eof = 0;
    }

    pFMC->buf_size   = len;

    {
        cookie_read_function_t* pRd = (pFMC->mode & FLAG_BIT(read))
            ?  (cookie_read_function_t*)fmem_read  : NULL;
        cookie_write_function_t* pWr = (pFMC->mode & FLAG_BIT(write))
            ? (cookie_write_function_t*)fmem_write : NULL;

        FILE* res;
#if defined(HAVE_FOPENCOOKIE)
        cookie_io_functions_t iof;
        iof.read  = pRd;
        iof.write = pWr;
        iof.seek  = (cookie_seek_function_t* )fmem_seek;
        iof.close = (cookie_close_function_t*)fmem_close;

        res = fopencookie( pFMC, pMode, iof );
#elif defined(HAVE_FUNOPEN)
        res = funopen( pFMC, pRd, pWr,
                       (cookie_seek_function_t* )fmem_seek,
                       (cookie_close_function_t*)fmem_close );
#else
#       include "We have neither fopencookie(3GNU) nor funopen(3BSD)"
#endif

        if (res == NULL) {
            if (pFMC->mode & FLAG_BIT(allocated))
                free( pFMC->buffer );
            free( pFMC );
        } else {
            int ix = AO_FMEM_HASH(pFMC->fp);
            pFMC->fp = res;
            pFMC->next = cookie_list[ ix ];
            cookie_list[ ix ] = pFMC;
        }

        return res;
    }
}

#endif /* ENABLE_FMEMOPEN */
/*
 * Local Variables:
 * mode: C
 * c-file-style: "stroustrup"
 * tab-width: 4
 * indent-tabs-mode: nil
 * End:
 * end of agen5/fmemopen.c */

--------------040105040206010301050101--