/*
#ident	"@(#)smail/src:RELEASE-3_2_0_121:parse.c,v 1.34 2005/07/06 23:56:41 woods Exp"
 */

/*
 *    Copyright (C) 1987, 1988 by Ronald S. Karr and Landon Curt Noll
 *    Copyright (C) 1992  Ronald S. Karr
 *
 * See the file COPYING, distributed with smail, for restriction
 * and warranty information.
 */

/*
 * parse.c:  (XXX this source file should be renames parsecfg.c!)
 *	Parse configuration files in a standard way.
 *
 *	The directory, router and transport files all share a common format
 *	which are parsed using routines in this file.  Although the format is
 *	slightly different, the rules for lexical tokens in the config files,
 *	as well as those for the method, retry, and qualify tables, are very
 *	much the same, so routines for parsing these files are provided as
 *	well.  The basic read_entry() and skip_space() routines are also used
 *	by the "lsearch" database lookup protocol, as well as the queryprog
 *	router driver.
 *
 *	external functions:  parse_entry, parse_config, parse_table,
 *	read_entry, skip_space.
 */

#include "defs.h"
#ifdef STANDALONE
# define xmalloc malloc
# define xrealloc realloc
# define xfree free
#endif	/* STANDALONE */

#include <sys/types.h>
#include <stdio.h>
#include <errno.h>
#include <ctype.h>

#ifdef STDC_HEADERS
# include <stdlib.h>
# include <stddef.h>
#else
# ifdef HAVE_STDLIB_H
#  include <stdlib.h>
# endif
#endif
#ifdef HAVE_STRING_H
# if !defined(STDC_HEADERS) && defined(HAVE_MEMORY_H)
#  include <memory.h>
# endif
# include <string.h>
#endif
#ifdef HAVE_STRINGS_H
# include <strings.h>
#endif
#ifdef __STDC__
# include <stdarg.h>
#else
# include <varargs.h>
#endif

#if defined(HAVE_UNISTD_H)
# include <unistd.h>
#endif

#include "smail.h"
#include "alloc.h"
#include "list.h"
#include "main.h"
#include "parse.h"
#include "addr.h"
#include "log.h"
#include "smailstring.h"
#include "dys.h"
#include "exitcodes.h"
#include "debug.h"
#include "extern.h"
#include "smailport.h"

/* variables exported from this file */
char *on = "1";				/* boolean on attribute value */
char *off = "0";			/* boolean off attribute value */

/* functions local to this file */
static char *finish_parse __P((char *, struct attribute **, struct attribute **));


/*
 * parse_entry - parse an entry from director, router or transport file
 *
 * given an entry from the given file, parse that entry, returning a vector
 * containing the NUL-terminated name of the entry followed by the name/value
 * pairs for all of the attributes in the entry.  Names which fit the form of a
 * boolean attribute will return a pointer to one of the external variables
 * "on" or "off".
 *
 * Returns the name of the entry on a successful read.  Returns NULL
 * on end of file or error.  For an error, a message is returned as an
 * error string.  Calling xfree on the name is sufficient to free all
 * of the string storage.  To free the list entries, step through the
 * lists and free each in turn.
 *
 * Note this code is very similar to the code in parse_config().
 */
char *
parse_entry(entry, generic, driver, error)
    register char *entry;		/* entry string to be parsed */
    struct attribute **generic;		/* all generic attributes */
    struct attribute **driver;		/* all driver attributes */
    char **error;			/* returned error message */
{
    char *start;
    struct str str;			/* string for processing entry */
    int this_attribute;			/* offset of attribute name */

    DEBUG2(DBG_CONF_HI, "parse_entry(): about to process entry that begins with: \n\t'%V'\n", (size_t) 70, entry);

    /* skip leading whitespace and comments */
    entry = skip_space(entry);

    *error = NULL;			/* no error yet! */
    start = entry;
    STR_INIT(&str);

    /*
     * grab the entry name as a collection of characters followed
     * by optional white space followed by a `:'
     */
    while (*entry && !isspace((int) *entry) && *entry != ':' && *entry != '#') {
	STR_NEXT(&str, *entry++);
    }
    STR_NEXT(&str, '\0');		/* NUL-terminate with the name */

    if (STR_LEN(&str) == 0) {
	*error = xprintf("missing field name (after entry beginning with \"%Q\")", (size_t) 40, start);
	DEBUG1(DBG_CONF_LO, "parse_entry:() %s\n", *error);
	STR_FREE(&str);
	return NULL;
    }

    /* skip spaces and comments after the name */
    entry = skip_space(entry);

    if (*entry != ':') {
	*error = "field name does not end in `:'";
	DEBUG1(DBG_CONF_LO, "parse_entry:() %s\n", *error);
	STR_FREE(&str);
	return NULL;
    }

    /* skip the `:' */
    entry++;

    /*
     * loop grabbing attributes and values until the end of
     * the entry
     *
     * XXX this code is very similar to the code below in parse_config()
     */
    while (*entry) {
	size_t name_offset;		/* temp */
	int boolean_flag;		/* `+' `-' or SPACE */

	boolean_flag = ' ';
	start = entry;

	/* skip spaces and comments before the attribute name (or `;') */
	entry = skip_space(entry);

	/* a semicolon separates generic and driver attributes */
	if (*entry == ';') {
	    /* skip spaces and comments after the semicolon */
	    entry = skip_space(entry + 1);
	    /* and if there's still anything afterward, record the semicolon */
	    if (*entry) {
		STR_NEXT(&str, ';');
	    }
	}

	/*
	 * be lenient about a `;' (or `,' from last go-around) with no
	 * following attributes
	 */
	if (*entry == '\0') {
	    break;
	}

	/* remember where this attribute setting starts in the parsed copy */
	this_attribute = STR_LEN(&str);

	/* if it's a boolean attribute then copy the value verbatim */
	if (*entry == '+' || *entry == '-') {
	    boolean_flag = *entry;
	    STR_NEXT(&str, *entry);
	    /* skip any spaces and comments after the flag */
	    entry = skip_space(entry + 1);
	}

	if (*entry == '\0' || *entry == ',' || *entry == ';') {
	    *error = xprintf("in entry %v: missing attribute name%s, in text beginning with \"%Q\"",
			     STR(&str),
			     boolean_flag != ' ' ? " after boolean flag" : "",
			     (size_t) 40, start);
	    DEBUG1(DBG_CONF_LO, "parse_entry:() %s\n", *error);
	    STR_FREE(&str);
	    return NULL;
	}

	/*
	 * scan over the attribute name and copy it to str
	 *
	 * an attribute name is of the form [+-]?[A-Za-z0-9_-]+
	 *
	 * XXX should probably require it to start with isalpha()...
	 */
	name_offset = STR_LEN(&str);
	while (*entry && (isalnum((int) *entry) || *entry == '_' || *entry == '-')) {
	    STR_NEXT(&str, *entry++);
	}
	if (STR_LEN(&str) == name_offset) {
	    *error = xprintf("missing variable name%s, in entry beginning with \"%Q\"",
			     boolean_flag != ' ' ? " after boolean flag" : "",
			     (size_t) 40, start);
	    DEBUG1(DBG_CONF_LO, "parse_config:() %s\n", *error);
	    return NULL;
	}
	STR_NEXT(&str, '\0');		/* terminate the attribute name */

	/* skip any spaces and comments after the end of the name */
	entry = skip_space(entry);

	if (*entry == '\0' || *entry == ',' || *entry == ';') {
	    /* end of a boolean attribute */
	    if (*entry == ',') {
		/* skip over commas */
		entry++;
	    }
	    continue;
	}

	if (*entry != '=') {
	    /* not boolean form and not "name = value" form */
	    *error = xprintf("in entry %v: attribute %v: expected `=' after the variable name",
			     STR(&str),
			     STR(&str) + this_attribute);
	    DEBUG1(DBG_CONF_LO, "parse_entry:() %s\n", *error);
	    STR_FREE(&str);
	    return NULL;
	}

	if (boolean_flag != ' ') {
	    /*
	     * if we found a leading `+' or `-' then there shouldn't be an
	     * assignment, but there was....
	     */
	    *error = xprintf("in entry %v: attribute %v: unexpected pattern `= value' follows boolean variable",
			     STR(&str),
			     STR(&str) + this_attribute);
	    DEBUG1(DBG_CONF_LO, "parse_entry:() %s\n", *error);
	    STR_FREE(&str);
	    return NULL;
	}

	/* note that this is a attribute value assignment */
	STR_NEXT(&str, '=');

	/* skip any spaces and comments after the '=' */
	entry = skip_space(entry + 1);

	if (*entry == '"') {
	    entry++;			/* skip the opening " */
	    /*
	     * if a quote, skip to the closing quote, following standard
	     * C convention with \-escapes.  Note that read_entry will
	     * have already done some processing for \ chars at the end of
	     * input lines.
	     */
	    while (*entry && *entry != '"') {
		if (*entry == '\\') {
		    int c;

		    entry = c_dequote(entry + 1, &c);
		    STR_NEXT(&str, c);
		} else {
		    STR_NEXT(&str, *entry++);
		}
	    }
	    if (*entry != '"') {
		/*
		 * make sure that the string doesn't suddenly come
		 * to an end at a funny spot
		 */
		*error = xprintf("in entry %v: attribute %v: missing closing quote for quoted value",
				 STR(&str),
				 STR(&str) + this_attribute);
		DEBUG1(DBG_CONF_LO, "parse_entry:() %s\n", *error);
		STR_FREE(&str);
		return NULL;
	    }
	    entry++;			/* skip that closing '"' */
	} else {
	    /*
	     * not in double quotes, only a limited set of characters
	     * are allowed in an unquoted string, though \ quotes any
	     * character.
	     */
	    while (*entry && (*entry == '\\' ||
			      strchr("!@$%^&*-_+~/?|<>:[]{}().`'", *entry) ||
			      isalnum((int) *entry)))
	    {
		if (*entry == '\\') {
		    entry++;
		    if (*entry == '\0') {
			/* must have something after \ */
			*error = xprintf("in entry %v: attribute %v: unexpected end of unquoted value after a backslash",
					 STR(&str),
					 STR(&str) + this_attribute);
			DEBUG1(DBG_CONF_LO, "parse_entry:() %s\n", *error);
			STR_FREE(&str);
			return NULL;
		    }
		}
		STR_NEXT(&str, *entry++);
	    }
	    /* note we check the proper termination below.... */
	}
	STR_NEXT(&str, '\0');		/* close off the value */

	/* skip any spaces and comments after the value */
	entry = skip_space(entry);

	/*
	 * make sure the entry ends in something reasonable
	 */
	if (*entry == ',') {
	    entry++;			/* commas are okay, and are ignored */
	} else if (*entry != ';') {
	    ;				/* semicolons are OK too, but are not ignored */
	} else if (*entry != '\0') {
	    /* anything else is not OK */
	    *error = xprintf("in entry %v: attribute %v: invalid attribute separator (%c)",
			     STR(&str),
			     STR(&str) + this_attribute,
			     *entry);
	    DEBUG1(DBG_CONF_LO, "parse_entry:() %s\n", *error);
	    STR_FREE(&str);
	    return NULL;
	}
    }
    STR_NEXT(&str, '\0');		/* two NUL bytes signal the entry's end */

    STR_DONE(&str);			/* finish off the string */

    /*
     * turn the entry, after the entry name, into the finished attribute lists
     */
    *error = finish_parse(STR(&str) + strlen(STR(&str)) + 1, generic, driver);

    if (*error) {
	DEBUG1(DBG_CONF_LO, "parse_entry:() finish_parse(): %s\n", *error);
	STR_FREE(&str);
	return NULL;			/* error found in finish_parse */
    }

    return STR(&str);			/* entry name was first */
}

/*
 * finish_parse - turn NUL-separated token strings into an attribute list
 *
 * return an error message or NULL, return generic and driver attributes
 * in the appropriate passed list pointers.
 */
static char *
finish_parse(tokens, generic, driver)
    register char *tokens;		/* strings of nul-terminated tokens */
    struct attribute **generic;		/* generic attributes go here */
    struct attribute **driver;		/* driver attributes go here */
{
    struct attribute **attr = generic;	/* begin adding generic attributes */
    *generic = NULL;
    *driver = NULL;

    /*
     * loop, snapping up tokens until no more remain
     */
    while (*tokens) {
	struct attribute *new;

	if (*tokens == ';') {
	    /* after `;' parse driver attributes */
	    attr = driver;
	    tokens++;			/* otherwise ignore `;' */
	}

	/*
	 * get a new token and link it into the output list
	 */
	new = (struct attribute *) xmalloc(sizeof(*new));
	new->succ = *attr;
	(*attr) = new;

	/* fill in the name */
	new->name = tokens;
	/* step to the next token */
	tokens = tokens + strlen(tokens) + 1;

	/* check for boolean attribute form */
	if (new->name[0] == '-' || new->name[0] == '+') {
	    /* boolean value */
	    if (*tokens == '=') {
		/* XXX haven't we already checked for this? */
		/* can't have both [+-] and a value */
		/* note new attribute is already linked into the list */
		return "mixed [+-]attribute and value assignment";
	    }

	    /*
	     * -name turns off attribute, +name turns it on
	     */
	    if (new->name[0] == '-') {
		new->value = off;
	    } else {
		new->value = on;
	    }
	    new->name++;		/* don't need [+-] anymore */
	} else {
	    if (*tokens == '=') {
		/* value token for attribute */
		new->value = tokens + 1; /* don't include `=' in the value */
		/* advance to the next token */
		tokens = tokens + strlen(tokens) + 1;
	    } else {
		/* just name is equivalent to +name */
		new->value = on;
	    }
	}
    }

    return NULL;
}


/*
 * parse_config - parse config file name/value pairs
 *
 * given a string, such as returned by read_entry, turn it into a single
 * attribute entry with the "name" field pointing at the start of the config
 * variable name and with the "value" field pointing at the variable's value.
 * Both are pointing at the same allocated storage block and can be freed by
 * passing the "name" pointer to xfree().
 *
 * On error, return NULL, with an error message in *error.
 *
 * XXX this code is very similar to the inner loop in parse_entry() above...
 */
struct attribute *
parse_config(entry, error)
    register char *entry;		/* config from read_entry */
    char **error;			/* return error message */
{
    char *start = entry;
    struct str str;			/* area for building result */
    int boolean_flag = ' ';		/* `+' `-' or SPACE */
    int value_offset;			/* offset in STR(&str) of value */
    struct attribute *attr;		/* new variable name/value struct */

    DEBUG2(DBG_CONF_HI, "parse_config(): about to process entry that begins with:\n\t'%V'\n", (size_t) 70, entry);

    /* variable setting begins at next non-white space character */
    entry = skip_space(entry);

    /* is this a boolean variable setting? */
    if (*entry == '+' || *entry == '-') {
	boolean_flag = *entry;
	/* skip any spaces and comments after the flag */
	entry = skip_space(entry + 1);
    }

    if (*entry == '\0') {
	*error = xprintf("missing variable name%s, in entry beginning with \"%Q\"",
			 boolean_flag != ' ' ? " after boolean flag" : "",
			 (size_t) 40, start);
	DEBUG1(DBG_CONF_LO, "parse_config:() %s\n", *error);
	return NULL;
    }

    attr = (struct attribute *) xmalloc(sizeof(*attr));
    attr->succ = NULL;
    STR_INIT(&str);

    /*
     * scan over the variable name and copy it to str
     *
     * we assume there's at least one valid non-space character here
     *
     * attribute name is of the form [A-Za-z0-9_-]+
     *
     * XXX should probably require it to start with isalpha()...
     */
    while (*entry && (isalnum((int) *entry) || *entry == '_' || *entry == '-')) {
	STR_NEXT(&str, *entry++);
    }
    if (STR_LEN(&str) == 0) {
	*error = xprintf("missing variable name%s, in entry beginning with \"%Q\"",
			 boolean_flag != ' ' ? " after boolean flag" : "",
			 (size_t) 40, start);
	DEBUG1(DBG_CONF_LO, "parse_config:() %s\n", *error);
	return NULL;
    }
    STR_NEXT(&str, '\0');		/* terminate variable name */

    /* skip any spaces and comments after the end of the name */
    entry = skip_space(entry);

    if (*entry == '\0') {
	/* boolean variable */
	STR_DONE(&str);
	attr->name = STR(&str);
	if (boolean_flag == '-') {
	    attr->value = off;
	} else {
	    attr->value = on;
	}

	return attr;
    }

    /* not boolean form and not "name = value" form */
    if (*entry != '=') {
	*error = xprintf("variable %v: expected `=' after the variable name", STR(&str));
	DEBUG1(DBG_CONF_LO, "parse_config:() %s\n", *error);
	xfree((char *) attr);
	STR_FREE(&str);
	return NULL;
    }

    if (boolean_flag != ' ') {
	/*
	 * if we found a leading `+' or `-' then there shouldn't be an
	 * assignment, but there was....
	 */
	*error = xprintf("variable %s: unexpected pattern `= value' follows a boolean variable", STR(&str));
	DEBUG1(DBG_CONF_LO, "parse_config:() %s\n", *error);
	xfree((char *) attr);
	STR_FREE(&str);
	return NULL;
    }

    /* skip any spaces after the '=' */
    entry = skip_space(entry + 1);

    /* form is `name = value', remember the start position of the value  */
    value_offset = STR_LEN(&str);

    /*
     * XXX this code is very nearly a duplicate of the code above in parse_entry()
     */
    if (*entry == '"') {
	entry++;		/* skip the opening " */
	/*
	 * if a quote, skip to the closing quote, following standard
	 * C convention with \-escapes.  Note that read_entry will
	 * have already done some processing for \ chars at the end of
	 * input lines.
	 */
	while (*entry && *entry != '"') {
	    if (*entry == '\\') {
		int c;

		entry = c_dequote(entry + 1, &c);
		STR_NEXT(&str, c);
	    } else {
		STR_NEXT(&str, *entry++);
	    }
	}
	if (*entry != '"') {
	    /*
	     * make sure that the string doesn't suddenly come
	     * to an end at a funny spot, normally it will be a NUL.
	     */
	    *error = xprintf("variable %v: missing closing quote for quoted value", STR(&str));
	    DEBUG1(DBG_CONF_LO, "parse_config:() %s\n", *error);
	    xfree((char *) attr);
	    STR_FREE(&str);
	    return NULL;
	}
	entry++;			/* skip that closing '"' */
    } else {
	/*
	 * not in double quotes, only a limited set of characters are allowed
	 * in an unquoted variable string (more than in an attribute string),
	 * though backslash (\) still quotes any character.
	 *
	 * Note too that in a config file we allow continuation of variable
	 * values without a trailing backslash.
	 */
	while (*entry && (*entry == '\\' || *entry == '\n' ||
			  strchr(" \t!@$%^&*-_=+~/?|<>;:[]{}(),.`'", *entry) || /* XXX " \t;,=" added */
			  isalnum((int) *entry)))
	{
	    if (*entry == '\n') {
		entry = skip_space(entry);
		if (*entry == '\0') {
		    break;		/* normal end of entry... */
		}
	    }
	    if (*entry == '\\') {
		entry++;
		if (*entry == '\0') {
		    /* must have something after \ */
		    *error = xprintf("variable %v: unexpected end of unquoted value after a backslash", STR(&str));
		    DEBUG1(DBG_CONF_LO, "parse_config:() %s\n", *error);
		    xfree((char *) attr);
		    STR_FREE(&str);
		    return NULL;
		}
	    }
	    STR_NEXT(&str, *entry++);
	}
	if (*entry != '\0') {
	    *error = xprintf("variable %v: unexpected, unescaped, special character '%c' in value, next 40 bytes of remaining text: \"%Q\"",
			     STR(&str),
			     *entry,
			     (size_t) 40, entry);
	    DEBUG1(DBG_CONF_LO, "parse_config:() %s\n", *error);
	    xfree((char *) attr);
	    STR_FREE(&str);
	    return NULL;
	}
    }
    STR_NEXT(&str, '\0');		/* close off the string after the value */

    /*
     * make sure this is really the end of the entry, first eating any trailing
     * whitespace and comments that would be ignored anyway....
     */
    entry = skip_space(entry);
    if (*entry != '\0') {
	*error = xprintf("variable %v: unexpected data after end of entry, next 40 bytes of remaining text: \"%Q\"",
			 STR(&str),
			 (size_t) 40, entry);
	DEBUG1(DBG_CONF_LO, "parse_config:() %s\n", *error);
	xfree((char *) attr);
	STR_FREE(&str);
	return NULL;
    }

    STR_DONE(&str);

    attr->name = STR(&str);
    attr->value = STR(&str) + value_offset;

    DEBUG3(DBG_CONF_HI, "parse_config(): got var `%v', with value that begins with:\n\t'%V'\n",
	   attr->name,
	   (size_t) 70, attr->value);

    return attr;
}


/*
 * parse_table - parse an entry in a table file
 *
 * table files have entries of the form:
 *
 *	string1		string2
 *
 * the returned "struct attribute" its "name" field pointing at the beginning
 * of string1 and its "value" field pointing at the beginning of string2.  Both
 * are pointing at the same allocated storage block and can be freed by passing
 * the "name" pointer to xfree().
 */
struct attribute *
parse_table(entry, error)
    register char *entry;		/* config from read_entry */
    char **error;			/* return error message */
{
    struct attribute *attr = (struct attribute *)xmalloc(sizeof(*attr));
    struct str str;
    int offset_transport;		/* offset to transport in STR(&str) */

    attr->succ = NULL;
    STR_INIT(&str);

    entry = skip_space(entry);
    while (*entry && !isspace((int) *entry) && *entry != '#') {
	STR_NEXT(&str, *entry++);
    }
    STR_NEXT(&str, '\0');		/* terminate name of host */

    entry = skip_space(entry);
    if (*entry == '\0') {
	*error = "unexpected end of entry";
	STR_FREE(&str);
	return NULL;
    }

    offset_transport = STR_LEN(&str);
    while (*entry && !isspace((int) *entry) && *entry != '#') {
	STR_NEXT(&str, *entry++);
    }
    STR_NEXT(&str, '\0');		/* terminate name of transport */

    entry = skip_space(entry);
    if (*entry) {
	*error = "expected end of entry";
	STR_FREE(&str);
	return NULL;
    }

    STR_DONE(&str);
    attr->name = STR(&str);
    attr->value = STR(&str) + offset_transport;

    return attr;
}

/*
 * skip_space - skip over comments and white space
 *
 * a comment is a `#' up to the end of a line
 */
char *
skip_space(p)
    register char *p;			/* current place in string */
{
    for (;;) {
	if (*p == '#') {
	    /* skip over comment */
	    p++;
	    while (*p && *p != '\n') {
		p++;
	    }
	} else if (!isspace((int) *p)) {
	    /* found something that isn't white space, return it */
	    return p;
	} else {
	    p++;			/* advance past the white-space char */
	}
    }
}


/*
 * read_entry - read a standard, possibly multi-line, entry from a file
 *
 * A "standard" entry, e.g. from a config, director, router, or transport
 * configuration file, or an entry from an alias file, is terminated by a line
 * which does not begin with whitespace.
 *
 * All comments are included in the returned text -- only "\\\n[ ]*" (escaped
 * newlines followed by optional whitespace) are stripped.  All comments at the
 * end of an entry are considered to be at the beginning of the next entry.
 *
 * return NULL on end of file or error.  The region return may be
 * reused for subsequent return values and should be copied if it
 * is to be preserved.
 *
 * XXX maybe we should try to count newlines and provide the line number of the
 * last line read.  That means we also need to keep track of the line number at
 * the trailing comment file position.  Note however that parse_config() and
 * similar routines deal with entries as single objects and so there will still
 * be no way to determine the actual line number of a parsing error in the
 * middle of an entry.  Perhaps the current technique of reporting the entry's
 * "name", as well as text leading up to (or following, as appropriate) the
 * error is good enough for users to find and understand the error in their raw
 * file format.
 */
char *
read_entry(f, infn)
    register FILE *f;			/* input file */
    char *infn;				/* input file name */
{
    register int c;			/* current character */
    static struct str str;		/* build the entry here */
    static int inited = FALSE;		/* TRUE if str has been STR_INIT'd */
    unsigned int ptcomment = 0;		/* possible trailing comment offset */
    fpos_t ptcompos;			/* trailing comment file position */

    /* 
     * Note, that str is initialized only once and then reused.
     */
    if (!inited) {
	inited = TRUE;
	STR_INIT(&str);
    } else {
	STR_CHECK(&str);
	STR_CLEAR(&str);
    }

    /*
     * scan for the beginning of an entry, which begins at the first
     * non-white space, non-comment character
     *
     * We do this in a separate loop since it makes the parsing of the entry
     * itself (to find its end) much easier (and also partly because once upon
     * a time this code skipped leading comments instead of including them in
     * the entry).
     */
    while ((c = getc(f)) != EOF && (isspace((int) c) || c == '#')) {
	STR_NEXT(&str, c);
	if (c == '#') {
	    while ((c = getc(f)) != EOF && c != '\n') {
		STR_NEXT(&str, c);
	    }
	    if (c == EOF) {
		break;
	    }
	    STR_NEXT(&str, c);		/* include the end-of-comment */
	}
    }

    /*
     * no entry was found
     */
    if (c == EOF) {
	return NULL;
    }

    STR_NEXT(&str, c);

    /*
     * continue copying characters up to the end of the entry.
     */
    while ((c = getc(f)) != EOF) {
	if (c == '\n') {
	    STR_NEXT(&str, c);
	    /*
	     * peek ahead to see what the next line starts with
	     */
	    c = getc(f);
	    /*
	     * end-of-file, or a line beginning with non-white space, marks the
	     * end of the current entry.
	     */
	    if (c == '\n' || c == '#') {
		/* blank lines and comments don't count */
		(void) ungetc(c, f);	/* unpeek */
		/*
		 * but we do want to remember where the blank lines or comments
		 * start in case this turns out to be the end of the entry so
		 * that we can trim them off and back up to read them as part
		 * of the next entry....
		 *
		 * Ideally we would like to trim only from a blank line (two
		 * consecutive newlines) onwards, i.e. consider comments
		 * "attached" to the end of the entry to be part of the entry,
		 * but the logic to do that gets a bit too complicated to be
		 * worthwhile.
		 */
		if (!ptcomment) {
		    ptcomment = STR_LEN(&str);
		    if (fgetpos(f, &ptcompos) != 0) {
			panic(EX_OSFILE, "fgetpos(%s): %s", infn, strerror(errno));
		    }
		}
		continue;		/* keep reading */
	    }
	    if (c == EOF || (c != ' ' && c != '\t')) {
		/* indeed this is the end of the entry! */
		break;
	    } else if (c != '#') {     /* XXX does this do anything? can it? */
		ptcomment = 0;
	    }
	}
	if (c == '\\') {
	    /* \newline is swallowed along with any following white-space */
	    if ((c = getc(f)) == EOF) {
		break;
	    }
	    if (c == '\n') {
		while ((c = getc(f)) == ' ' || c == '\t') {
		    ;
		}
	    } else {
		STR_NEXT(&str, '\\');
	    }
	}
	STR_NEXT(&str, c);
    }

    /*
     * that's the end of that entry
     */
    if (c != EOF) {
	(void) ungetc(c, f);		/* first character for the next entry? */
    }
    /*
     * trim off any trailing blank lines and/or comments and back up to read
     * them as part of the next entry
     */
    if (ptcomment) {
	STR_TRIM(&str, ptcomment);
	if (c != EOF) {
	    if (fsetpos(f, &ptcompos) != 0) {
		panic(EX_OSFILE, "fsetpos(%s): %s", infn, strerror(errno));
	    }
	}
    }
    STR_NEXT(&str, '\0');		/* end of the entry */

    return STR(&str);
}

#ifdef STANDALONE
/*
 * read from standard input and write out the compiled
 * entry information on the standard output
 */
void
main(argc, argv)
    int argc;				/* count of arguments */
    char *argv[];			/* vector of arguments */
{
    char *entry;			/* entry read from stdin */
    enum { config, table, other } file_type; /* type of file to look at */

    if (argc >= 2 && EQ(argv[1], "-c")) {
	file_type = config;
    } else if (argc >= 2 && EQ(argv[1], "-t")) {
	file_type = table;
    } else {
	file_type = other;
    }

    /*
     * read entries until EOF
     */
    while (entry = read_entry(stdin, "stdin")) {
	if (file_type == config) {
	    char *error;
	    struct attribute *attr = parse_config(entry, &error);

	    if (attr == NULL) {
		(void) fprintf(stderr, "error in <stdin>: %s\n", error);
		exit(EX_DATAERR);
	    }
	    (void) printf("%s = %s\n", attr->name, quote(attr->value));
	} else if (file_type == table) {
	    char *error;
	    struct attribute *attr = parse_table(entry, &error);

	    if (attr == NULL) {
		(void) fprintf(stderr, "error in <stdin>: %s\n", error);
		exit(EX_DATAERR);
	    }
	    (void) printf("%s = %s\n", attr->name, quote(attr->value));
	} else {
	    struct attribute *generic;	/* generic attribute list */
	    struct attribute *driver;	/* driver attribute list */
	    char *error;		/* error message */
	    char *name = parse_entry(entry, &generic, &driver, &error);

	    if (name == NULL) {
		(void) fprintf(stderr, "error in <stdin>: %s\n", error);
		exit(EX_DATAERR);
	    }
	    (void) printf("Entry Name:  %s:\n    Generic Attributes:\n", name);
	    while (generic) {
		(void) printf("\t%s = %s\n", generic->name, quote(generic->value));
		generic = generic->succ;
	    }
	    (void) printf("    Driver Attributes:\n");
	    while (driver) {
		(void) printf("\t%s = %s\n", driver->name, quote(driver->value));
		driver = driver->succ;
	    }
	}
    }

    exit(EX_OK);
}
#endif	/* STANDALONE */

/* 
 * Local Variables:
 * c-file-style: "smail"
 * End:
 */


syntax highlighted by Code2HTML, v. 0.9.1