/*
 * Copyright (c) 1992, Brian Berliner and Jeff Polk
 * Copyright (c) 1989-1992, Brian Berliner
 * 
 * You may distribute under the terms of the GNU General Public License as
 * specified in the README file that comes with the CVS 1.4 kit.
 */

#include "cvs.h"
#include "getline.h"
#include <assert.h>

/*
 * Parse the INFOFILE file for the specified REPOSITORY.  Invoke CALLPROC for
 * the first line in the file that matches the REPOSITORY, or if ALL != 0, any lines
 * matching "ALL", or if no lines match, the last line matching "DEFAULT".
 *
 * Return 0 for success, -1 if there was not an INFOFILE, and >0 for failure.
 */
int
Parse_Info (infofile, repository, callproc, all)
    char *infofile;
    char *repository;
    CALLPROC callproc;
    int all;
{
    int err = 0;
    FILE *fp_info;
    char *infopath;
    char *line = NULL;
    size_t line_allocated = 0;
    char *default_value = NULL;
    char *expanded_value= NULL;
    int callback_done, line_number;
    char *cp, *exp, *value, *srepos;
    const char *regex_err;

    if (CVSroot_original == NULL)
    {
	/* XXX - should be error maybe? */
	error (0, 0, "CVSROOT variable not set");
	return (1);
    }

    /* find the info file and open it */
    infopath = xmalloc (strlen (CVSroot_directory)
			+ strlen (infofile)
			+ sizeof (CVSROOTADM)
			+ 10);
    (void) sprintf (infopath, "%s/%s/%s", CVSroot_directory,
		    CVSROOTADM, infofile);
    fp_info = CVS_FOPEN (infopath, "r");
    if (fp_info == NULL)
    {
	/* If no file, don't do anything special.  */
	if (!existence_error (errno))
	    error (0, errno, "cannot open %s", infopath);
	free (infopath);
	return 0;
    }

    /* strip off the CVSROOT if repository was absolute */
    srepos = Short_Repository (repository);

    if (trace)
	(void) fprintf (stderr, "-> ParseInfo(%s, %s, %s)\n",
			infopath, srepos, all ? "ALL" : "not ALL");

    /* search the info file for lines that match */
    callback_done = line_number = 0;
    while (getline (&line, &line_allocated, fp_info) >= 0)
    {
	line_number++;

	/* skip lines starting with # */
	if (line[0] == '#')
	    continue;

	/* skip whitespace at beginning of line */
	for (cp = line; *cp && isspace (*cp); cp++)
	    ;

	/* if *cp is null, the whole line was blank */
	if (*cp == '\0')
	    continue;

	/* the regular expression is everything up to the first space */
	for (exp = cp; *cp && !isspace (*cp); cp++)
	    ;
	if (*cp != '\0')
	    *cp++ = '\0';

	/* skip whitespace up to the start of the matching value */
	while (*cp && isspace (*cp))
	    cp++;

	/* no value to match with the regular expression is an error */
	if (*cp == '\0')
	{
	    error (0, 0, "syntax error at line %d file %s; ignored",
		   line_number, infofile);
	    continue;
	}
	value = cp;

	/* strip the newline off the end of the value */
	if ((cp = strrchr (value, '\n')) != NULL)
	    *cp = '\0';

	if (expanded_value != NULL)
	    free (expanded_value);
	expanded_value = expand_path (value, infofile, line_number);
	if (!expanded_value)
	{
	    continue;
	}

	/*
	 * At this point, exp points to the regular expression, and value
	 * points to the value to call the callback routine with.  Evaluate
	 * the regular expression against srepos and callback with the value
	 * if it matches.
	 */

	/* save the default value so we have it later if we need it */
	if (strcmp (exp, "DEFAULT") == 0)
	{
	    /* Is it OK to silently ignore all but the last DEFAULT
               expression?  */
	    if (default_value != NULL)
		free (default_value);
	    default_value = xstrdup (expanded_value);
	    continue;
	}

	/*
	 * For a regular expression of "ALL", do the callback always We may
	 * execute lots of ALL callbacks in addition to *one* regular matching
	 * callback or default
	 */
	if (strcmp (exp, "ALL") == 0)
	{
	    if (all)
		err += callproc (repository, expanded_value);
	    else
		error(0, 0, "Keyword `ALL' is ignored at line %d in %s file",
		      line_number, infofile);
	    continue;
	}

	if (callback_done)
	    /* only first matching, plus "ALL"'s */
	    continue;

	/* see if the repository matched this regular expression */
	if ((regex_err = re_comp (exp)) != NULL)
	{
	    error (0, 0, "bad regular expression at line %d file %s: %s",
		   line_number, infofile, regex_err);
	    continue;
	}
	if (re_exec (srepos) == 0)
	    continue;				/* no match */

	/* it did, so do the callback and note that we did one */
	err += callproc (repository, expanded_value);
	callback_done = 1;
    }
    if (ferror (fp_info))
	error (0, errno, "cannot read %s", infopath);
    if (fclose (fp_info) < 0)
	error (0, errno, "cannot close %s", infopath);

    /* if we fell through and didn't callback at all, do the default */
    if (callback_done == 0 && default_value != NULL)
	err += callproc (repository, default_value);

    /* free up space if necessary */
    if (default_value != NULL)
	free (default_value);
    if (expanded_value != NULL)
	free (expanded_value);
    free (infopath);
    if (line != NULL)
	free (line);

    return (err);
}


/* Parse the CVS config file.  The syntax right now is a bit ad hoc
   but tries to draw on the best or more common features of the other
   *info files and various unix (or non-unix) config file syntaxes.
   Lines starting with # are comments.  Settings are lines of the form
   KEYWORD=VALUE.  There is currently no way to have a multi-line
   VALUE (would be nice if there was, probably).

   CVSROOT is the $CVSROOT directory (CVSroot_directory might not be
   set yet).

   Returns 0 for success, negative value for failure.  Call
   error(0, ...) on errors in addition to the return value.  */
int
parse_config (cvsroot)
    char *cvsroot;
{
    char *infopath;
    FILE *fp_info;
    char *line = NULL;
    size_t line_allocated = 0;
    size_t len;
    char *p;
    /* FIXME-reentrancy: If we do a multi-threaded server, this would need
       to go to the per-connection data structures.  */
    static int parsed = 0;

    /* Authentication code and serve_root might both want to call us.
       Let this happen smoothly.  */
    if (parsed)
	return 0;
    parsed = 1;

    infopath = malloc (strlen (cvsroot)
			+ sizeof (CVSROOTADM_CONFIG)
			+ sizeof (CVSROOTADM)
			+ 10);
    if (infopath == NULL)
    {
	error (0, 0, "out of memory; cannot allocate infopath");
	goto error_return;
    }

    strcpy (infopath, cvsroot);
    strcat (infopath, "/");
    strcat (infopath, CVSROOTADM);
    strcat (infopath, "/");
    strcat (infopath, CVSROOTADM_CONFIG);

    fp_info = CVS_FOPEN (infopath, "r");
    if (fp_info == NULL)
    {
	/* If no file, don't do anything special.  */
	if (!existence_error (errno))
	{
	    /* Just a warning message; doesn't affect return
	       value, currently at least.  */
	    error (0, errno, "cannot open %s", infopath);
	}
	free (infopath);
	return 0;
    }

    while (getline (&line, &line_allocated, fp_info) >= 0)
    {
	/* Skip comments.  */
	if (line[0] == '#')
	    continue;

	/* At least for the moment we don't skip whitespace at the start
	   of the line.  Too picky?  Maybe.  But being insufficiently
	   picky leads to all sorts of confusion, and it is a lot easier
	   to start out picky and relax it than the other way around.

	   Is there any kind of written standard for the syntax of this
	   sort of config file?  Anywhere in POSIX for example (I guess
	   makefiles are sort of close)?  Red Hat Linux has a bunch of
	   these too (with some GUI tools which edit them)...

	   Along the same lines, we might want a table of keywords,
	   with various types (boolean, string, &c), as a mechanism
	   for making sure the syntax is consistent.  Any good examples
	   to follow there (Apache?)?  */

	/* Strip the training newline.  There will be one unless we
	   read a partial line without a newline, and then got end of
	   file (or error?).  */

	len = strlen (line) - 1;
	if (line[len] == '\n')
	    line[len] = '\0';

	/* Skip blank lines.  */
	if (line[0] == '\0')
	    continue;

	/* The first '=' separates keyword from value.  */
	p = strchr (line, '=');
	if (p == NULL)
	{
	    /* Probably should be printing line number.  */
	    error (0, 0, "syntax error in %s: line '%s' is missing '='",
		   infopath, line);
	    goto error_return;
	}

	*p++ = '\0';

	if (strcmp (line, "RCSBIN") == 0)
	{
	    if (free_Rcsbin)
		free (Rcsbin);

	    len = strlen (p);
	    /* 2 is for '/' and '\0'.  */
	    Rcsbin = malloc (len + 2);
	    if (Rcsbin == NULL)
	    {
		free_Rcsbin = 0;
		/* Could be a fatal error, I guess, except we don't
		   currently have the machinery to do fatal error from
		   here.  */
		error (0, ENOMEM, "Cannot allocate Rcsbin");
		goto error_return;
	    }
	    free_Rcsbin = 1;
	    strcpy (Rcsbin, p);

	    /* If Rcsbin does not end in a ISDIRSEP character, add a
	       trailing slash.  */
	    if (!ISDIRSEP (Rcsbin[len - 1]))
	    {
		Rcsbin[len] = '/';
		Rcsbin[len + 1] = '\0';
	    }
	}
	else if (strcmp (line, "SystemAuth") == 0)
	{
	    if (strcmp (p, "no") == 0)
#ifdef AUTH_SERVER_SUPPORT
		system_auth = 0;
#else
		/* Still parse the syntax but ignore the
		   option.  That way the same config file can
		   be used for local and server.  */
		;
#endif
	    else if (strcmp (p, "yes") == 0)
#ifdef AUTH_SERVER_SUPPORT
		system_auth = 1;
#else
		;
#endif
	    else
	    {
		error (0, 0, "unrecognized value '%s' for SystemAuth", p);
		goto error_return;
	    }
	}
	else
	{
	    error (0, 0, "%s: unrecognized keyword '%s'",
		   infopath, line);
	    goto error_return;
	}
    }
    if (ferror (fp_info))
    {
	error (0, errno, "cannot read %s", infopath);
	goto error_return;
    }
    if (fclose (fp_info) < 0)
    {
	error (0, errno, "cannot close %s", infopath);
	goto error_return;
    }
    free (infopath);
    if (line != NULL)
	free (line);
    return 0;

 error_return:
    if (infopath != NULL)
	free (infopath);
    if (line != NULL)
	free (line);
    return -1;
}


/* parse_etc_config: Parse the (optional) system wide /etc/cvs.conf file.
 * ( only valid/parsed when in pserver mode )
 * Below comment can be directly snipped out to make an example /etc/cvs.conf

-------8<--------------8<--------------8<--------------8<--------------8<----
# This is an example /etc/cvs.conf for CVS in pserver mode
# be carefull to edit it befor using.
#
# Here you may define the CVSROOT, that are allowed for remote sides and
# other global server setting ( only valid/parsed when in pserver mode )
# The format is just one line per definition, Empty lines and lines
# starting with '#' are ignored. White spaces (blank,tab) are removed.
# After the keyword (e.g. allow-root) '=' is taken as whitespace.
#
# Currently the following keywords are recognized:
#
# 1.  allow-root
# 2.  cvs-exec-prefix
# 3.  cvs-admin-group
# 4.  force-gzip-level
#
# 1.  allow-root
# --------------
# Global 'allow-root' definitions are valuable, such that
# they need not to be in /etc/inetd.conf (where the number of them
# may be limited) and is only called when running in pserver mode.
#
# If you want CVS-pserver do chroot, separate the path with '/./',
# The path before '/./' is the path to which will be chroot'ed,
# the part after that is the path that will be valid _after_
# chroot has been called and this one also is what the user has
# to pass as repository.
#
# A typical setup for CVS in pserver mode is:
#
# /etc/services
#    cvspserver      2401/tcp        # CVS pserver
#
# /etc/inetd.conf
#    cvspserver stream tcp nowait root /usr/bin/cvs cvs pserver
#
# /etc/cvs.conf
#    allow-root =  /CVS/billystuff
#    allow-root /mycvs/CVSROOT
#    allow-root=/home2/cvs_chroot/./test
#
# Note:
# for the last example (/home2...) the user has login such like this
#    cvs -d :pserver:guest@cvs.server.host:/test
# and on cvs.server.host the pserver will chroot to /home2/cvs_chroot
# before executing any command.
#
# 
# 2.  cvs-exec-prefix
# -------------------
# With the keyword 'cvs-exec-prefix' you may define a prefix, that
# is appended to all program names (except those allready prefixed by
# the Rcsbin prefix), that CVS executes as result of its processing.
# That may be 'diff', 'patch' (as set via DIFF and PATCH_PROGRAM in
# options.h.in) e.t.c., but this may also be a binary that an intruder
# may have successfully placed somewhere accessable.
# Using the cvs-exec-prefix, the intruder program can't be executed.
# However, this forces you to have _all_ scripts and executables
# (except the RCSbin prefixed ones) in this (readonly) directory
# and to avoid abslolute paths in CVSROOT/commitinfo. You also can't
# then use any of the 'prog' options in CVSROOT/modules.
# Also, don't keep this directory below your repository, else
# cvs-exec-prefix could be tricked out. 
#
#   example:
#
#      cvs-exec-prefix = /usr/bin
#
#
# 3.  cvs-admin-group
# -------------------
# With the keyword 'cvs-admin-group' you may change or disable
# the CVS_ADMIN_GROUP checking (as predefined in src/options.h).
# If the value is 'no' or 'none' or empty, it disables otherwise
# it redefines the group.
#
#   examples:
#
#      cvs-admin-group = no
#   or
#      cvs-admin-group = mygroup
#
#
# 4.  force-gzip-level
# --------------------
# With the keyword 'force-gzip-level' you may force the pserver
# to reject any uncompressed transfer with the client. This is
# valuable, if you want to protect your server from netword overload.
# In this case, the server will refuse any service if the client did
# not used -z option.
#
#   examples:
#
#      force-gzip-level=9
#   or
#      force-gzip-level      ... which will have level 6 per default
#
-------8<--------------8<--------------8<--------------8<--------------8<----

*/


void parse_etc_config (void)
{
    FILE *fp;
    char *line = NULL;
    size_t line_allocated = 0;
    int linecount = 0;
    int len; /* NOTE: don't use size_t as this is unsigned !! */
    char *p;

    if (!isfile("/etc/cvs.conf"))
	return;

    fp = CVS_FOPEN ("/etc/cvs.conf", "r");
    if (!fp)
	return;

    while ((len = getline(&line, &line_allocated, fp)) > 0)
    {
	linecount++;
	/* Strip the trailing newline. */
	if (line[len-1] == '\n')
	    line[len-1] = '\0';
	/* remove leading whitespaces */
	p = line + strspn(line," \t");
	
	/* Skip comments.  */
	if (p[0] == '#')
	    continue;

	/* Skip blank lines.  */
	if (line[0] == '\0')
	    continue;

#define IF_KEYWORD_MATCH(keyw) \
	if (!strncmp(p, keyw, sizeof(keyw)-1)) { \
	    p += sizeof(keyw)-1;	/* after the keyword */ \
	    p += strspn(p, " \t=");	/* skip leading whitespaces */ \
	    p[strcspn(p, " \t=")] = 0;  /* truncate trailing whitespaces */
#define ENDIF_KEYWORD_MATCH \
	    continue; \
	}

	IF_KEYWORD_MATCH("allow-root")
	    if (*p)
		root_allow_add(p);
	ENDIF_KEYWORD_MATCH

	IF_KEYWORD_MATCH("cvs-exec-prefix")
	    if (*p)
		cvs_exec_prefix = xstrdup(p);
	ENDIF_KEYWORD_MATCH

	IF_KEYWORD_MATCH("cvs-admin-group")
	    cvs_admin_group = NULL;
	    if (*p && strcmp(p, "no") && strcmp(p, "none"))
		cvs_admin_group = xstrdup(p);
	ENDIF_KEYWORD_MATCH

	IF_KEYWORD_MATCH("force-gzip-level")
	    if (*p) {
		server_wanted_gzip_level = atol(p);
		if (server_wanted_gzip_level > 9)
		    server_wanted_gzip_level = 9;
		else if (server_wanted_gzip_level <= 0)
		    server_wanted_gzip_level = 6;
	    }
	ENDIF_KEYWORD_MATCH

	error (0, 0, "syntax error in /etc/cvs.conf line: %d", linecount);
	break;
    }
    fclose(fp);
    if (line != NULL)
	free (line);
}
