vbuf_print.c   [plain text]


/*++
/* NAME
/*	vbuf_print 3
/* SUMMARY
/*	formatted print to generic buffer
/* SYNOPSIS
/*	#include <stdarg.h>
/*	#include <vbuf_print.h>
/*
/*	VBUF	*vbuf_print(bp, format, ap)
/*	VBUF	*bp;
/*	const char *format;
/*	va_list	ap;
/* DESCRIPTION
/*	vbuf_print() appends data to the named buffer according to its
/*	\fIformat\fR argument. It understands the s, c, d, u, o, x, X, p, e,
/*	f and g format types, the l modifier, field width and precision,
/*	sign, and padding with zeros or spaces.
/*
/*	In addition, vbuf_print() recognizes the %m format specifier
/*	and expands it to the error message corresponding to the current
/*	value of the global \fIerrno\fR variable.
/* REENTRANCY
/* .ad
/* .fi
/*	vbuf_print() allocates a static buffer. After completion
/*	of the first vbuf_print() call, this buffer is safe for
/*	reentrant vbuf_print() calls by (asynchronous) terminating
/*	signal handlers or by (synchronous) terminating error
/*	handlers. vbuf_print() initialization typically happens
/*	upon the first formatted output to a VSTRING or VSTREAM.
/*
/*	However, it is up to the caller to ensure that the destination
/*	VSTREAM or VSTRING buffer is protected against reentrant usage.
/* LICENSE
/* .ad
/* .fi
/*	The Secure Mailer license must be distributed with this software.
/* AUTHOR(S)
/*	Wietse Venema
/*	IBM T.J. Watson Research
/*	P.O. Box 704
/*	Yorktown Heights, NY 10598, USA
/*
/*	Wietse Venema
/*	Google, Inc.
/*	111 8th Avenue
/*	New York, NY 10011, USA
/*--*/

/* System library. */

#include "sys_defs.h"
#include <stdlib.h>			/* 44BSD stdarg.h uses abort() */
#include <stdarg.h>
#include <string.h>
#include <ctype.h>
#include <stdlib.h>			/* 44bsd stdarg.h uses abort() */
#include <stdio.h>			/* sprintf() prototype */
#include <float.h>			/* range of doubles */
#include <errno.h>
#include <limits.h>			/* CHAR_BIT, INT_MAX */

/* Application-specific. */

#include "msg.h"
#include "vbuf.h"
#include "vstring.h"
#include "vbuf_print.h"

 /*
  * What we need here is a *sprintf() routine that can ask for more room (as
  * in 4.4 BSD). However, that functionality is not widely available, and I
  * have no plans to maintain a complete 4.4 BSD *sprintf() alternative.
  * 
  * Postfix vbuf_print() was implemented when many mainstream systems had no
  * usable snprintf() implementation (usable means: return the length,
  * excluding terminator, that the output would have if the buffer were large
  * enough). For example, GLIBC before 2.1 (1999) snprintf() did not
  * distinguish between formatting error and buffer size error, while SUN had
  * no snprintf() implementation before Solaris 2.6 (1997).
  * 
  * For the above reasons, vbuf_print() was implemented with sprintf() and a
  * generously-sized output buffer. Current vbuf_print() implementations use
  * snprintf(), and report an error if the output does not fit (in that case,
  * the old sprintf()-based implementation would have had a buffer overflow
  * vulnerability). The old implementation is still available for building
  * Postfix on ancient systems.
  * 
  * Guessing the output size of a string (%s) conversion is not hard. The
  * problem is with numerical results. Instead of making an accurate guess we
  * take a wide margin when reserving space.  The INT_SPACE margin should be
  * large enough to hold the result from any (octal, hex, decimal) integer
  * conversion that has no explicit width or precision specifiers. With
  * floating-point numbers, use a similar estimate, and add DBL_MAX_10_EXP
  * just to be sure.
  */
#define INT_SPACE	((CHAR_BIT * sizeof(long)) / 2)
#define DBL_SPACE	((CHAR_BIT * sizeof(double)) / 2 + DBL_MAX_10_EXP)
#define PTR_SPACE	((CHAR_BIT * sizeof(char *)) / 2)

 /*
  * Helper macros... Note that there is no need to check the result from
  * VSTRING_SPACE() because that always succeeds or never returns.
  */
#ifndef NO_SNPRINTF
#define VBUF_SNPRINTF(bp, sz, fmt, arg) do { \
	ssize_t _ret; \
	VBUF_SPACE((bp), (sz)); \
	_ret = snprintf((char *) (bp)->ptr, (bp)->cnt, (fmt), (arg)); \
	if (_ret < 0) \
	    msg_panic("%s: output error for '%s'", myname, (fmt)); \
	if (_ret >= (bp)->cnt) \
	    msg_panic("%s: output for '%s' exceeds space %ld", \
		      myname, fmt, (long) (bp)->cnt); \
	VBUF_SKIP(bp); \
    } while (0)
#else
#define VBUF_SNPRINTF(bp, sz, fmt, arg) do { \
	VBUF_SPACE((bp), (sz)); \
	sprintf((char *) (bp)->ptr, (fmt), (arg)); \
	VBUF_SKIP(bp); \
    } while (0)
#endif

#define VBUF_SKIP(bp) do { \
	while ((bp)->cnt > 0 && *(bp)->ptr) \
	    (bp)->ptr++, (bp)->cnt--; \
    } while (0)

#define VSTRING_ADDNUM(vp, n) do { \
	VBUF_SNPRINTF(&(vp)->vbuf, INT_SPACE, "%d", n); \
    } while (0)

#define VBUF_STRCAT(bp, s) do { \
	unsigned char *_cp = (unsigned char *) (s); \
	int _ch; \
	while ((_ch = *_cp++) != 0) \
	    VBUF_PUT((bp), _ch); \
    } while (0)

/* vbuf_print - format string, vsprintf-like interface */

VBUF   *vbuf_print(VBUF *bp, const char *format, va_list ap)
{
    const char *myname = "vbuf_print";
    static VSTRING *fmt;		/* format specifier */
    unsigned char *cp;
    int     width;			/* width and numerical precision */
    int     prec;			/* are signed for overflow defense */
    unsigned long_flag;			/* long or plain integer */
    int     ch;
    char   *s;
    int     saved_errno = errno;	/* VBUF_SPACE() may clobber it */

    /*
     * Assume that format strings are short.
     */
    if (fmt == 0)
	fmt = vstring_alloc(INT_SPACE);

    /*
     * Iterate over characters in the format string, picking up arguments
     * when format specifiers are found.
     */
    for (cp = (unsigned char *) format; *cp; cp++) {
	if (*cp != '%') {
	    VBUF_PUT(bp, *cp);			/* ordinary character */
	} else if (cp[1] == '%') {
	    VBUF_PUT(bp, *cp++);		/* %% becomes % */
	} else {

	    /*
	     * Handle format specifiers one at a time, since we can only deal
	     * with arguments one at a time. Try to determine the end of the
	     * format specifier. We do not attempt to fully parse format
	     * strings, since we are ging to let sprintf() do the hard work.
	     * In regular expression notation, we recognize:
	     * 
	     * %-?+?0?([0-9]+|\*)?(\.([0-9]+|\*))?l?[a-zA-Z]
	     * 
	     * which includes some combinations that do not make sense. Garbage
	     * in, garbage out.
	     */
	    VSTRING_RESET(fmt);			/* clear format string */
	    VSTRING_ADDCH(fmt, *cp++);
	    if (*cp == '-')			/* left-adjusted field? */
		VSTRING_ADDCH(fmt, *cp++);
	    if (*cp == '+')			/* signed field? */
		VSTRING_ADDCH(fmt, *cp++);
	    if (*cp == '0')			/* zero-padded field? */
		VSTRING_ADDCH(fmt, *cp++);
	    if (*cp == '*') {			/* dynamic field width */
		width = va_arg(ap, int);
		VSTRING_ADDNUM(fmt, width);
		cp++;
	    } else {				/* hard-coded field width */
		for (width = 0; ch = *cp, ISDIGIT(ch); cp++) {
		    int     digit = ch - '0';

		    if (width > INT_MAX / 10
			|| (width *= 10) > INT_MAX - digit)
			msg_panic("%s: bad width %d... in %.50s",
				  myname, width, format);
		    width += digit;
		    VSTRING_ADDCH(fmt, ch);
		}
	    }
	    if (*cp == '.') {			/* width/precision separator */
		VSTRING_ADDCH(fmt, *cp++);
		if (*cp == '*') {		/* dynamic precision */
		    prec = va_arg(ap, int);
		    VSTRING_ADDNUM(fmt, prec);
		    cp++;
		} else {			/* hard-coded precision */
		    for (prec = 0; ch = *cp, ISDIGIT(ch); cp++) {
			int     digit = ch - '0';

			if (prec > INT_MAX / 10
			    || (prec *= 10) > INT_MAX - digit)
			    msg_panic("%s: bad precision %d... in %.50s",
				      myname, prec, format);
			prec += digit;
			VSTRING_ADDCH(fmt, ch);
		    }
		}
	    } else {
		prec = -1;
	    }
	    if ((long_flag = (*cp == 'l')) != 0)/* long whatever */
		VSTRING_ADDCH(fmt, *cp++);
	    if (*cp == 0)			/* premature end, punt */
		break;
	    VSTRING_ADDCH(fmt, *cp);		/* type (checked below) */
	    VSTRING_TERMINATE(fmt);		/* null terminate */

	    /*
	     * Execute the format string - let sprintf() do the hard work for
	     * non-trivial cases only. For simple string conversions and for
	     * long string conversions, do a direct copy to the output
	     * buffer.
	     */
	    switch (*cp) {
	    case 's':				/* string-valued argument */
		if (long_flag)
		    msg_panic("%s: %%l%c is not supported", myname, *cp);
		s = va_arg(ap, char *);
		if (prec >= 0 || (width > 0 && width > strlen(s))) {
		    VBUF_SNPRINTF(bp, (width > prec ? width : prec) + INT_SPACE,
				  vstring_str(fmt), s);
		} else {
		    VBUF_STRCAT(bp, s);
		}
		break;
	    case 'c':				/* integral-valued argument */
		if (long_flag)
		    msg_panic("%s: %%l%c is not supported", myname, *cp);
		/* FALLTHROUGH */
	    case 'd':
	    case 'u':
	    case 'o':
	    case 'x':
	    case 'X':
		if (long_flag)
		    VBUF_SNPRINTF(bp, (width > prec ? width : prec) + INT_SPACE,
				  vstring_str(fmt), va_arg(ap, long));
		else
		    VBUF_SNPRINTF(bp, (width > prec ? width : prec) + INT_SPACE,
				  vstring_str(fmt), va_arg(ap, int));
		break;
	    case 'e':				/* float-valued argument */
	    case 'f':
	    case 'g':
		/* C99 *printf ignore the 'l' modifier. */
		VBUF_SNPRINTF(bp, (width > prec ? width : prec) + DBL_SPACE,
			      vstring_str(fmt), va_arg(ap, double));
		break;
	    case 'm':
		/* Ignore the 'l' modifier, width and precision. */
		VBUF_STRCAT(bp, strerror(saved_errno));
		break;
	    case 'p':
		if (long_flag)
		    msg_panic("%s: %%l%c is not supported", myname, *cp);
		VBUF_SNPRINTF(bp, (width > prec ? width : prec) + PTR_SPACE,
			      vstring_str(fmt), va_arg(ap, char *));
		break;
	    default:				/* anything else is bad */
		msg_panic("vbuf_print: unknown format type: %c", *cp);
		/* NOTREACHED */
		break;
	    }
	}
    }
    return (bp);
}

#ifdef TEST
#include <argv.h>
#include <msg_vstream.h>
#include <vstring.h>
#include <vstring_vstream.h>

int     main(int argc, char **argv)
{
    VSTRING *ibuf = vstring_alloc(100);

    msg_vstream_init(argv[0], VSTREAM_ERR);

    while (vstring_fgets_nonl(ibuf, VSTREAM_IN)) {
	ARGV   *args = argv_split(vstring_str(ibuf), CHARS_SPACE);
	char   *cp;

	if (args->argc == 0 || *(cp = args->argv[0]) == '#') {
	     /* void */ ;
	} else if (args->argc != 2 || *cp != '%') {
	    msg_warn("usage: format number");
	} else {
	    char   *fmt = cp++;
	    int     lflag;

	    /* Determine the vstring_sprintf() argument type. */
	    cp += strspn(cp, "+-*0123456789.");
	    if ((lflag = (*cp == 'l')) != 0)
		cp++;
	    if (cp[1] != 0) {
		msg_warn("bad format: \"%s\"", fmt);
	    } else {
		VSTRING *obuf = vstring_alloc(1);
		char   *val = args->argv[1];

		/* Test the worst-case memory allocation. */
#ifdef CA_VSTRING_CTL_EXACT
		vstring_ctl(obuf, CA_VSTRING_CTL_EXACT, CA_VSTRING_CTL_END);
#endif
		switch (*cp) {
		case 'c':
		case 'd':
		case 'o':
		case 'u':
		case 'x':
		case 'X':
		    if (lflag)
			vstring_sprintf(obuf, fmt, atol(val));
		    else
			vstring_sprintf(obuf, fmt, atoi(val));
		    msg_info("\"%s\"", vstring_str(obuf));
		    break;
		case 's':
		    vstring_sprintf(obuf, fmt, val);
		    msg_info("\"%s\"", vstring_str(obuf));
		    break;
		case 'f':
		case 'g':
		    vstring_sprintf(obuf, fmt, atof(val));
		    msg_info("\"%s\"", vstring_str(obuf));
		    break;
		default:
		    msg_warn("bad format: \"%s\"", fmt);
		    break;
		}
		vstring_free(obuf);
	    }
	}
	argv_free(args);
    }
    vstring_free(ibuf);
    return (0);
}

#endif