/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License (the "License").
 * You may not use this file except in compliance with the License.
 *
 * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
 * or http://www.opensolaris.org/os/licensing.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information: Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 */

/*
 * Copyright (c) 1997, 2010, Oracle and/or its affiliates. All rights reserved.
 */

#include <ctype.h>
#include <errno.h>
#include <locale.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <strings.h>
#include <string.h>
#include <syslog.h>
#include <nfs/nfs.h>
#include <assert.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include "nfslog_config.h"

#define	ERROR_BUFSZ	100

/*
 * This flag controls where error messages go.
 * Zero means that messages go to stderr.
 * Non-zero means that messages go to syslog.
 */
boolean_t nfsl_errs_to_syslog;

/*
 * Pointer to the global entry in the list
 */
static nfsl_config_t *global = NULL;

/*
 * Pointer to the raw global entry in the list, this is the
 * global entry without the expanded paths. This is used to
 * complete configurations.
 */
static nfsl_config_t *global_raw = NULL;

/*
 * Last modification time to config file.
 */
static timestruc_t config_last_modification = { 0 };

/*
 * Whitespace characters to delimit fields in a line.
 */
static const char *whitespace = " \t";

static int getconfiglist(nfsl_config_t **, boolean_t);
static nfsl_config_t *create_config(char *, char *, char *, char *, char *,
			char *, int, boolean_t, int *);
static nfsl_config_t *create_global_raw(int *);
static int update_config(nfsl_config_t *, char *, char *, char *,
			char *, char *, char *, int, boolean_t, boolean_t);
static int update_field(char **, char *, char *, boolean_t *);
static nfsl_config_t *findconfig(nfsl_config_t **, char *, boolean_t,
			nfsl_config_t **);
static nfsl_config_t *getlastconfig(nfsl_config_t *);
static void complete_with_global(char **, char **, char **, char **,
			char **, int *);
#ifdef DEBUG
static void remove_config(nfsl_config_t **, nfsl_config_t *, nfsl_config_t **);
void nfsl_printconfig(nfsl_config_t *);
#endif /* DEBUG */
static char *gataline(FILE *, char *, char *, int);
static int get_info(char *, char **, char **, char **, char **, char **,
			char **, int *);
static void free_config(nfsl_config_t *);
static int is_legal_tag(char *);
static boolean_t is_complete_config(char *, char *, char *, char *);

/*
 * Read the configuration file and create a list of configuration
 * parameters.  Returns zero for success or an errno value.
 * The caller is responsible for freeing the returned configlist by calling
 * nfsl_freeconfig_list().
 *
 * If the configuration file does not exist, *listpp points to a config entry
 * containing the hardwired defaults.
 */
int
nfsl_getconfig_list(nfsl_config_t **listpp)
{
	int error = 0;
	char *locale;

	/*
	 * Set the locale correctly so that we can correctly identify
	 * alphabetic characters.
	 */
	if ((locale = getenv("LC_ALL")) != NULL)
		(void) setlocale(LC_ALL, locale);
	else if ((locale = getenv("LC_CTYPE")) != NULL)
		(void) setlocale(LC_CTYPE, locale);
	else if ((locale = getenv("LANG")) != NULL)
		(void) setlocale(LC_CTYPE, locale);

	/*
	 * Allocate 'global_raw' structure, its contents are
	 * indirectly allocated by create_config().
	 */
	assert(global_raw == NULL);
	global_raw = create_global_raw(&error);
	if (global_raw == NULL)
		return (error);

	/*
	 * Build global entry with hardwired defaults first.
	 */
	assert(global == NULL);
	global = create_config(DEFAULTTAG, DEFAULTDIR, BUFFERPATH, NULL,
			FHPATH, LOGPATH, TRANSLOG_BASIC, B_TRUE, &error);
	*listpp = global;
	if (global == NULL) {
		free_config(global_raw);
		return (error);
	}

	if (error = getconfiglist(listpp, B_FALSE))
		nfsl_freeconfig_list(listpp);
	else {
		assert(global != NULL);
		/*
		 * The global entry was replaced with the one in the file,
		 * clear the UPDATED flag
		 */
		global->nc_flags &= ~NC_UPDATED;
	}
	return (error);
}

/*
 * Allocates memory for the 'global_raw' structure.
 * The actual allocation of values for its components happens in
 * update_config().
 */
static nfsl_config_t *
create_global_raw(int *error)
{
	nfsl_config_t *p;

	*error = 0;
	if (p = (nfsl_config_t *)malloc(sizeof (*p)))
		(void) memset((void *)p, 0, sizeof (*p));
	else
		*error = ENOMEM;

	return (p);
}

/*
 * Checks if the the configuration file has been modified since we last
 * read it, if not simply returns, otherwise it re-reads it adding new
 * configuration entries. Note that existing entries that no longer
 * exist in the configuration file are not removed. Existing entries
 * that are modified in the configuration file are updated in the list
 * as well.
 * if 'updated' is defined then it is set to TRUE if the list was modified.
 *
 * Note that if an error occurs, the list may be corrupted.
 * It is the responsibility of the caller to free the list.
 * If the configuration file does not exist, we simply return the list
 * that we previously had, log a message and return success.
 */
int
nfsl_checkconfig_list(nfsl_config_t **listpp, boolean_t *updated)
{
	struct stat st;
	int error = 0;

	if (updated != NULL)
		*updated = B_FALSE;

	if (stat(NFSL_CONFIG_FILE_PATH, &st) == -1) {
		error = errno;
		if (nfsl_errs_to_syslog) {
			syslog(LOG_ERR, gettext(
				"Can't stat %s - %s"), NFSL_CONFIG_FILE_PATH,
				strerror(error));
		} else {
			(void) fprintf(stderr, gettext(
				"Can't stat %s - %s\n"), NFSL_CONFIG_FILE_PATH,
				strerror(error));
		}
		return (0);
	}

	if (config_last_modification.tv_sec == st.st_mtim.tv_sec &&
	    config_last_modification.tv_nsec == st.st_mtim.tv_nsec)
		return (0);

	if (updated != NULL)
		*updated = B_TRUE;

	return (getconfiglist(listpp, B_TRUE));
}

/*
 * Does the real work. Reads the configuration file and creates the
 * list of entries. Assumes that *listpp contains at least one entry.
 * The caller is responsible for freeing any config entries added to
 * the list whether this routine returns an error or not.
 *
 * Returns 0 on success and updates the '*listpp' config list,
 * Returns non-zero error value otherwise.
 */
static int
getconfiglist(nfsl_config_t **listpp, boolean_t updating)
{
	FILE *fp;
	int error = 0;
	nfsl_config_t *listp = NULL, *tail = NULL;
	char linebuf[MAX_LINESZ];
	char errorbuf[ERROR_BUFSZ];
	char *tag, *defaultdir, *bufferpath, *rpclogpath, *fhpath, *logpath;
	int logformat;
	flock_t flock;
	struct stat st;

	fp = fopen(NFSL_CONFIG_FILE_PATH, "r");
	if (fp == NULL) {
		if (updating) {
			(void) sprintf(errorbuf, "Can't open %s",
				NFSL_CONFIG_FILE_PATH);
		} else {
			(void) sprintf(errorbuf,
				"Can't open %s - using hardwired defaults",
				NFSL_CONFIG_FILE_PATH);
		}

		/*
		 * Use hardwired config.
		 */
		if (nfsl_errs_to_syslog)
			syslog(LOG_ERR, gettext("%s"), errorbuf);
		else
			(void) fprintf(stderr, gettext("%s\n"), errorbuf);

		return (0);
	}

	(void) memset((void *) &flock, 0, sizeof (flock));
	flock.l_type = F_RDLCK;
	if (fcntl(fileno(fp), F_SETLKW, &flock) == -1) {
		error = errno;
		if (nfsl_errs_to_syslog) {
			syslog(LOG_ERR, gettext(
				"Can't lock %s - %s"), NFSL_CONFIG_FILE_PATH,
				strerror(error));
		} else {
			(void) fprintf(stderr, gettext(
				"Can't lock %s - %s\n"), NFSL_CONFIG_FILE_PATH,
				strerror(error));
		}
		goto done;
	}

	assert (*listpp != NULL);
	tail = getlastconfig(*listpp);

	while (gataline(fp, NFSL_CONFIG_FILE_PATH, linebuf, sizeof (linebuf))) {
		if (linebuf[0] == '\0') {
			/*
			 * ignore lines that exceed max size
			 */
			continue;
		}

		if (error = get_info(linebuf, &tag, &defaultdir, &bufferpath,
		    &rpclogpath, &fhpath, &logpath, &logformat))
			break;

		if (listp = findconfig(listpp, tag, B_FALSE, &tail)) {
			/*
			 * An entry with the same tag name exists,
			 * update the fields that changed.
			 */
			error = update_config(listp, tag, defaultdir,
					bufferpath, rpclogpath, fhpath, logpath,
					logformat, B_TRUE, B_TRUE);
			if (error)
				break;
		} else {
			/*
			 * New entry, create it.
			 */
			listp = create_config(tag, defaultdir,
					bufferpath, rpclogpath, fhpath,
					logpath, logformat, B_TRUE, &error);
			if (listp == NULL)
				break;

			if (*listpp == NULL)
				*listpp = listp;
			else
				tail->nc_next = listp;
			tail = listp;
		}

		assert(global != NULL);
	}

	if (!error) {
		/*
		 * Get mtime while we have file locked
		 */
		if (error = fstat(fileno(fp), &st)) {
			error = errno;
			if (nfsl_errs_to_syslog) {
				syslog(LOG_ERR, gettext(
				"Can't stat %s - %s"), NFSL_CONFIG_FILE_PATH,
				strerror(error));
			} else {
				(void) fprintf(stderr, gettext(
				"Can't stat %s - %s\n"), NFSL_CONFIG_FILE_PATH,
				strerror(error));
			}
		}
		config_last_modification = st.st_mtim;
	}

done:
	(void) fclose(fp);
	return (error);
}

/*
 * Creates the config structure with the values specified by the
 * parameters. If defaultdir has been specified, all relative paths
 * are prepended with this defaultdir.
 * If 'complete' is set then this must represent a complete config entry
 * as specified by is_complete_config(), otherwise no work is perfomed, and
 * NULL is returned.
 *
 * Returns the newly created config structure on success.
 * Returns NULL on failure and sets error to the appropriate error.
 */
static nfsl_config_t *
create_config(
	char *tag,
	char *defaultdir,
	char *bufferpath,
	char *rpclogpath,
	char *fhpath,
	char *logpath,
	int   logformat,
	boolean_t complete,
	int  *error)
{
	nfsl_config_t *config;

	if ((config = (nfsl_config_t *)malloc(sizeof (*config))) == NULL) {
		*error = ENOMEM;
		return (NULL);
	}
	(void) memset((void *)config, 0, sizeof (*config));

	*error = update_config(config, tag, defaultdir, bufferpath, rpclogpath,
			fhpath, logpath, logformat, complete, B_TRUE);
	if (*error) {
		free(config);
		return (NULL);
	}

	config->nc_flags &= ~NC_UPDATED;	/* This is a new entry */

	return (config);
}


/*
 * Updates the configuration entry with the new information provided,
 * sets NC_UPDATED to indicate so. The entry is left untouched if all
 * the fields are the same (except for 'nc_rpccookie', 'nc_transcookie'
 * and 'nc_next').
 * Prepends each path component with 'defauldir' if 'prepend' is set.
 *
 * Returns 0 on success, error otherwise.
 * On error, the config entry is left in an inconsistent state.
 * The only thing the caller can really do with it is free it.
 */
static int
update_config(
	nfsl_config_t *config,
	char *tag,
	char *defaultdir,
	char *bufferpath,
	char *rpclogpath,
	char *fhpath,
	char *logpath,
	int   logformat,
	boolean_t complete,
	boolean_t prepend)
{
	boolean_t updated, config_updated = B_FALSE;
	int error = 0;

	if (complete && !is_complete_config(tag, bufferpath, fhpath, logpath)) {
		/*
		 * Not a complete entry
		 */
		if (nfsl_errs_to_syslog) {
			syslog(LOG_ERR, gettext(
			"update_config: \"%s\" not a complete config entry."),
			tag);
		} else {
			(void) fprintf(stderr, gettext(
			"update_config: \"%s\" not a complete config entry.\n"),
			tag);
		}
		return (EINVAL);
	}

	assert(tag != NULL);
	if (config->nc_name == NULL) {
		/*
		 * New entry
		 */
		if ((config->nc_name = strdup(tag)) == NULL) {
			error = ENOMEM;
			goto errout;
		}
	} else
		assert(strcmp(config->nc_name, tag) == 0);

	if (error = update_field(
	    &config->nc_defaultdir, defaultdir, NULL, &updated))
		goto errout;
	if (!prepend) {
		/*
		 * Do not prepend default directory.
		 */
		defaultdir = NULL;
	}
	config_updated |= updated;
	if (error = update_field(
	    &config->nc_bufferpath, bufferpath, defaultdir, &updated))
		goto errout;
	config_updated |= updated;
	if (error = update_field(
	    &config->nc_rpclogpath, rpclogpath, defaultdir, &updated))
		goto errout;
	config_updated |= updated;
	if (error = update_field(
	    &config->nc_fhpath, fhpath, defaultdir, &updated))
		goto errout;
	config_updated |= updated;
	if (error = update_field(
	    &config->nc_logpath, logpath, defaultdir, &updated))
		goto errout;
	config_updated |= updated;
	updated = (config->nc_logformat != logformat);
	if (updated)
		config->nc_logformat = logformat;
	config_updated |= updated;

	if (config_updated)
		config->nc_flags |= NC_UPDATED;

	if (strcmp(tag, DEFAULTTAG) == 0) {
		/*
		 * Have the default global config point to this entry.
		 */
		global = config;

		/*
		 * Update the global_raw configuration entry.
		 * Make sure no expanding of paths occurs.
		 */
		if (error = update_config(global_raw, DEFAULTRAWTAG, defaultdir,
			bufferpath, rpclogpath, fhpath, logpath, logformat,
			complete, B_FALSE))
				goto errout;
	}

	return (error);

errout:
	if (nfsl_errs_to_syslog) {
		syslog(LOG_ERR, gettext(
			"update_config: Can't process \"%s\" config entry: %s"),
			tag, strerror(error));
	} else {
		(void) fprintf(stderr, gettext(
		"update_config: Can't process \"%s\" config entry: %s\n"),
		tag, strerror(error));
	}
	return (error);
}

/*
 * Prepends 'prependir' to 'new' if 'prependir' is defined.
 * Compares the value of '*old' with 'new', if it has changed,
 * then sets whatever 'old' references equal to 'new'.
 * Returns 0 on success, error otherwise.
 * Sets '*updated' to B_TRUE if field was modified.
 * The value of '*updated' is undefined on error.
 */
static int
update_field(
	char **old,		/* pointer to config field */
	char *new,		/* updated value */
	char *prependdir,	/* prepend this directory to new */
	boolean_t *updated)	/* field was modified */
{
	char *tmp_new = NULL;
	int need_update = 0;

	if (new != NULL) {
		if (prependdir != NULL && new[0] != '/') {
			tmp_new = malloc(strlen(prependdir) + strlen(new) + 2);
			if (tmp_new == NULL)
				return (ENOMEM);
			(void) sprintf(tmp_new, "%s/%s", prependdir, new);
		} else {
			if ((tmp_new = strdup(new)) == NULL)
				return (ENOMEM);
		}
	}

	if (tmp_new != NULL) {
		if (*old == NULL)
			need_update++;
		else if (strcmp(tmp_new, *old) != 0) {
			free(*old);
			need_update++;
		}
		if (need_update)
			*old = tmp_new;
	} else if (*old != NULL) {
		need_update++;
		free(*old);
		*old = NULL;
	}

	*updated = need_update != 0;
	return (0);
}

#ifdef DEBUG
/*
 * Removes and frees the 'config' entry from the list
 * pointed to by '*listpp'.
 * No error is reported if the entry does not exist.
 * Updates '*tail' to point to the last item in the list.
 */
static void
remove_config(
	nfsl_config_t **listpp,
	nfsl_config_t *config,
	nfsl_config_t **tail)
{
	nfsl_config_t *p, *prev;

	prev = *listpp;
	for (p = *listpp; p != NULL; p = p->nc_next) {
		if (p == config) {
			if (p == prev) {
				/*
				 * first element of the list
				 */
				*listpp = prev->nc_next;
			} else
				prev->nc_next = p->nc_next;
			free_config(p);
			break;
		}
		prev = p;
	}

	/*
	 * Find tail of the list.
	 */
	for (*tail = prev; (*tail)->nc_next != NULL; *tail = (*tail)->nc_next)
		;
}
#endif /* DEBUG */

static void
free_config(nfsl_config_t *config)
{
	if (config == NULL)
		return;
	if (config->nc_name)
		free(config->nc_name);
	if (config->nc_defaultdir)
		free(config->nc_defaultdir);
	if (config->nc_bufferpath)
		free(config->nc_bufferpath);
	if (config->nc_rpclogpath)
		free(config->nc_rpclogpath);
	if (config->nc_fhpath)
		free(config->nc_fhpath);
	if (config->nc_logpath)
		free(config->nc_logpath);
	if (config == global)
		global = NULL;
	if (config == global_raw)
		global_raw = NULL;
	free(config);
}

void
nfsl_freeconfig_list(nfsl_config_t **listpp)
{
	nfsl_config_t *next;

	if (*listpp == NULL)
		return;

	do {
		next = (*listpp)->nc_next;
		free_config(*listpp);
		*listpp = next;
	} while (*listpp);

	free_config(global_raw);
}

/*
 * Returns a pointer to the first instance of 'tag' in the list.
 * If 'remove' is true, then the entry is removed from the list and
 * a pointer to it is returned.
 * If '*tail' is not NULL, then it will point to the last element of
 * the list. Note that this function assumes that *tail already
 * points at the last element of the list.
 * Returns NULL if the entry does not exist.
 */
static nfsl_config_t *
findconfig(
	nfsl_config_t **listpp,
	char *tag, boolean_t remove,
	nfsl_config_t **tail)
{
	nfsl_config_t *p, *prev;

	prev = *listpp;
	for (p = *listpp; p != NULL; p = p->nc_next) {
		if (strcmp(p->nc_name, tag) == 0) {
			if (remove) {
				if (p == prev) {
					/*
					 * first element of the list
					 */
					*listpp = prev->nc_next;
				} else
					prev->nc_next = p->nc_next;

				if (tail != NULL && p == *tail) {
					/*
					 * Only update *tail if we removed
					 * the last element of the list, and we
					 * requested *tail to be updated.
					 */
					*tail = prev;
				}
			}
			return (p);
		}
		prev = p;
	}

	return (NULL);
}

static nfsl_config_t *
getlastconfig(nfsl_config_t *listp)
{
	nfsl_config_t *lastp = NULL;

	for (; listp != NULL; listp = listp->nc_next)
		lastp = listp;

	return (lastp);
}

/*
 * Returns a pointer to the first instance of 'tag' in the list.
 * Returns NULL if the entry does not exist.
 * Sets 'error' if the update of the list failed if necessary, and
 * returns NULL.
 */
nfsl_config_t *
nfsl_findconfig(nfsl_config_t *listp, char *tag, int *error)
{
	nfsl_config_t *config;
	boolean_t updated;

	*error = 0;
	config = findconfig(&listp, tag, B_FALSE, (nfsl_config_t **)NULL);
	if (config == NULL) {
		/*
		 * Rebuild our list if the file has changed.
		 */
		if (*error = nfsl_checkconfig_list(&listp, &updated)) {
			/*
			 * List may be corrupted, notify caller.
			 */
			return (NULL);
		}
		if (updated) {
			/*
			 * Search for tag again.
			 */
			config = findconfig(&listp, tag, B_FALSE,
				(nfsl_config_t **)NULL);
		}
	}

	return (config);
}

/*
 * Use the raw global values if any of the parameters is not defined.
 */
static void
complete_with_global(
	char **defaultdir,
	char **bufferpath,
	char **rpclogpath,
	char **fhpath,
	char **logpath,
	int  *logformat)
{
	if (*defaultdir == NULL)
		*defaultdir = global_raw->nc_defaultdir;
	if (*bufferpath == NULL)
		*bufferpath = global_raw->nc_bufferpath;
	if (*rpclogpath == NULL)
		*rpclogpath = global_raw->nc_rpclogpath;
	if (*fhpath == NULL)
		*fhpath = global_raw->nc_fhpath;
	if (*logpath == NULL)
		*logpath = global_raw->nc_logpath;
	if (*logformat == 0)
		*logformat = global_raw->nc_logformat;
}

/*
 * Parses 'linebuf'. Returns 0 if a valid tag is found, otherwise non-zero.
 * Unknown tokens are silently ignored.
 * It is the responsibility of the caller to make a copy of the non-NULL
 * parameters if they need to be used before linebuf is freed.
 */
static int
get_info(
	char *linebuf,
	char **tag,
	char **defaultdir,
	char **bufferpath,
	char **rpclogpath,
	char **fhpath,
	char **logpath,
	int  *logformat)
{
	char *tok;
	char *tmp;

	/* tag */
	*tag = NULL;
	tok = strtok(linebuf, whitespace);
	if (tok == NULL)
		goto badtag;
	if (!is_legal_tag(tok))
		goto badtag;
	*tag = tok;

	*defaultdir = *bufferpath = *rpclogpath = NULL;
	*fhpath = *logpath = NULL;
	*logformat = 0;

	while (tok = strtok(NULL, whitespace)) {
		if (strncmp(tok, "defaultdir=", strlen("defaultdir=")) == 0) {
			*defaultdir = tok + strlen("defaultdir=");
		} else if (strncmp(tok, "buffer=", strlen("buffer=")) == 0) {
			*bufferpath = tok + strlen("buffer=");
		} else if (strncmp(tok, "rpclog=", strlen("rpclog=")) == 0) {
			*rpclogpath = tok + strlen("rpclog=");
		} else if (strncmp(tok, "fhtable=", strlen("fhtable=")) == 0) {
			*fhpath = tok + strlen("fhtable=");
		} else if (strncmp(tok, "log=", strlen("log=")) == 0) {
			*logpath = tok + strlen("log=");
		} else if (strncmp(tok, "logformat=",
				strlen("logformat=")) == 0) {
			tmp = tok + strlen("logformat=");
			if (strncmp(tmp, "extended", strlen("extended")) == 0) {
				*logformat = TRANSLOG_EXTENDED;
			} else {
				/*
				 * Use transaction log basic format if
				 * 'extended' was not specified.
				 */
				*logformat = TRANSLOG_BASIC;
			}
		}
	}

	if (strcmp(*tag, DEFAULTTAG) != 0) {
		/*
		 * Use global values for fields not specified if
		 * this tag is not the global tag.
		 */
		complete_with_global(defaultdir, bufferpath,
			rpclogpath, fhpath, logpath, logformat);
	}

	return (0);

badtag:
	if (nfsl_errs_to_syslog) {
		syslog(LOG_ERR, gettext(
			"Bad tag found in config file."));
	} else {
		(void) fprintf(stderr, gettext(
			"Bad tag found in config file.\n"));
	}
	return (-1);
}

/*
 * Returns True if we have all the elements of a complete configuration
 * entry. A complete configuration has tag, bufferpath, fhpath and logpath
 * defined to non-zero strings.
 */
static boolean_t
is_complete_config(
	char *tag,
	char *bufferpath,
	char *fhpath,
	char *logpath)
{
	assert(tag != NULL);
	assert(strlen(tag) > 0);

	if ((bufferpath != NULL && strlen(bufferpath) > 0) &&
	    (fhpath != NULL && strlen(fhpath) > 0) &&
	    (logpath != NULL && strlen(logpath) > 0))
		return (B_TRUE);
	return (B_FALSE);
}

#ifdef DEBUG
/*
 * Prints the configuration entry to stdout.
 */
void
nfsl_printconfig(nfsl_config_t *config)
{
	if (config->nc_name)
		(void) printf("tag=%s\t", config->nc_name);
	if (config->nc_defaultdir)
		(void) printf("defaultdir=%s\t", config->nc_defaultdir);
	if (config->nc_logpath)
		(void) printf("logpath=%s\t", config->nc_logpath);
	if (config->nc_fhpath)
		(void) printf("fhpath=%s\t", config->nc_fhpath);
	if (config->nc_bufferpath)
		(void) printf("bufpath=%s\t", config->nc_bufferpath);
	if (config->nc_rpclogpath)
		(void) printf("rpclogpath=%s\t", config->nc_rpclogpath);
	if (config->nc_logformat == TRANSLOG_BASIC)
		(void) printf("logformat=basic");
	else if (config->nc_logformat == TRANSLOG_EXTENDED)
		(void) printf("logformat=extended");
	else
		(void) printf("config->nc_logformat=UNKNOWN");

	if (config->nc_flags & NC_UPDATED)
		(void) printf("\tflags=NC_UPDATED");
	(void) printf("\n");
}

/*
 * Prints the configuration list to stdout.
 */
void
nfsl_printconfig_list(nfsl_config_t *listp)
{
	for (; listp != NULL; listp = listp->nc_next) {
		nfsl_printconfig(listp);
		(void) printf("\n");
	}
}
#endif /* DEBUG */

/*
 * Returns non-zero if the given string is allowable for a tag, zero if
 * not.
 */
static int
is_legal_tag(char *tag)
{
	int i;
	int len;

	if (tag == NULL)
		return (0);
	len = strlen(tag);
	if (len == 0)
		return (0);

	for (i = 0; i < len; i++) {
		char c;

		c = tag[i];
		if (!(isalnum((unsigned char)c) || c == '_'))
			return (0);
	}

	return (1);
}

/*
 * gataline attempts to get a line from the configuration file,
 * upto LINESZ. A line in the file is a concatenation of lines if the
 * continuation symbol '\' is used at the end of the line. Returns
 * line on success, a NULL on EOF, and an empty string on lines > linesz.
 */
static char *
gataline(FILE *fp, char *path, char *line, int linesz) {
	register char *p = line;
	register int len;
	int excess = 0;

	*p = '\0';

	for (;;) {
		if (fgets(p, linesz - (p-line), fp) == NULL) {
			return (*line ? line : NULL);   /* EOF */
		}

		len = strlen(line);
		if (len <= 0) {
			p = line;
			continue;
		}
		p = &line[len - 1];

		/*
		 * Is input line too long?
		 */
		if (*p != '\n') {
			excess = 1;
			/*
			 * Perhaps last char read was '\'. Reinsert it
			 * into the stream to ease the parsing when we
			 * read the rest of the line to discard.
			 */
			(void) ungetc(*p, fp);
			break;
		}
trim:

		/* trim trailing white space */
		while (p >= line && isspace(*(uchar_t *)p))
		*p-- = '\0';
		if (p < line) {			/* empty line */
			p = line;
			continue;
		}

		if (*p == '\\') {		/* continuation */
			*p = '\0';
			continue;
		}

		/*
		 * Ignore comments. Comments start with '#'
		 * which must be preceded by a whitespace, unless
		 * '#' is the first character in the line.
		 */
		p = line;

		while (p = strchr(p, '#')) {
			if (p == line || isspace(*(p-1))) {
				*p-- = '\0';
				goto trim;
			}
			p++;
		}

		break;
	}
	if (excess) {
		int c;

		/*
		 * discard rest of line and return an empty string.
		 * done to set the stream to the correct place when
		 * we are done with this line.
		 */
		while ((c = getc(fp)) != EOF) {
			*p = c;
			if (*p == '\n')		/* end of the long line */
				break;
			else if (*p == '\\') {		/* continuation */
				if (getc(fp) == EOF)	/* ignore next char */
					break;
			}
		}
		if (nfsl_errs_to_syslog) {
			syslog(LOG_ERR, gettext(
				"%s: line too long - ignored (max %d chars)"),
				path, linesz-1);
		} else {
			(void) fprintf(stderr, gettext(
				"%s: line too long - ignored (max %d chars)\n"),
				path, linesz-1);
		}
		*line = '\0';
	}

	return (line);
}