/*
#ident	"@(#)smail/src:RELEASE-3_2_0_120:match.c,v 1.24 2004/08/26 18:39:11 woods Exp"
 */

/*
 *    Copyright (C) 2004  Greg A. Woods
 *
 * See the file COPYING, distributed with smail, for restriction
 * and warranty information.
 *
 * Some or all of this code may also be released under a separate copyright
 * license.  Contact the author for details.
 */

/*
 * match.c:
 *	routines for matching values in various ways.
 *
 *	external functions:  match_re, match_ip, match_ip_net, flip_inet_addr,
 *			     match_re_list, compile_pcre_list, match_pcre_list,
 *			     free_pcre_list, format_pcre_list
 */

#include "defs.h"
#ifdef STANDALONE
# undef HAVE_LIBWHOSON
#endif

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

#ifdef STDC_HEADERS
# include <stdlib.h>
# include <stddef.h>
#else
# ifdef HAVE_STDLIB_H
#  include <stdlib.h>
# endif
#endif

#ifdef __STDC__
# include <stdarg.h>
#else
# include <varargs.h>
#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

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

#ifdef HAVE_LIBWHOSON
# include <whoson.h>
#endif

#include <pcre.h>
#include <pcreposix.h>			/* PCRE's POSIX-like API */

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

/* functions local to this file */
static pcre_info_t *compile_pcre_expr __P((char *, int, char *, char *, int, char **));
static int match_pcre_expr __P((char *, size_t, pcre_info_t *, int, char **));
static char *pcre_expand_msg __P((pcre_info_t *, char *, int *, int));


/*
 * match_re - match a string against a regular expression using the POSIX API
 *
 * NOTE:  If icase is non-zero then the goal is to do case-insensitive matching
 * by default.
 *
 * RETURNS:
 *
 *	MATCH_MATCHED	- the string matched the RE
 *	MATCH_NOMATCH	- no match.
 *	MATCH_FAIL	- an error occured, error message written to paniclog
 */
int
match_re(key, exp, icase, start, beg, end, last)
    char *key;			/* string to match */
    char *exp;			/* RE to match against */
    int icase;			/* ignore case? */
    char **start;		/* optional ptr to beg of match */
    char **beg;			/* optional ptr to beg of sub-expression */
    char **end;			/* optional ptr to after sub-expression */
    char **last;		/* optional ptr to after match */
{
    regex_t preg;
    char re_errmsg[BUFSIZ];
    regmatch_t pmatch[2];
    int  rc;

    DEBUG3(DBG_DRIVER_HI, "match_re: matching '%v' against expression '%v'%s\n",
	   key, exp, icase ? ", ignoring case" : "");

    /* XXX should consider converting to using the native PCRE API, perhaps
     * compile_pcre_expr() and match_pcre_expr()... */
    if ((rc = regcomp(&preg, exp, (REG_EXTENDED |
				   (icase ? REG_ICASE : 0) |
				   ((beg || end) ? 0 : REG_NOSUB))))) {
	(void) regerror(rc, &preg, re_errmsg, sizeof(re_errmsg));
	write_log(WRITE_LOG_PANIC, "match_re: bad regular expression '%v': %s",
		  exp, re_errmsg);
	regfree(&preg);
	return MATCH_FAIL;
    }
    /* zero indicates match */
    switch ((rc = regexec(&preg, key, (size_t) 2, pmatch, 0))) {
    case 0:
	rc = MATCH_MATCHED;
	if (start) {
	    *start = key + pmatch[0].rm_so;
	}
	/* if (pmatch[1].rm_so == -1) assert(pmatch[1].rm_eo == -1); */
	if (beg) {
	    *beg = key + ((pmatch[1].rm_so == -1) ? pmatch[0].rm_so : pmatch[1].rm_so);
	}
	if (end) {
	    *end = key + ((pmatch[1].rm_eo == -1) ? pmatch[0].rm_eo : pmatch[1].rm_eo);
	}
	if (last) {
	    *last = key + pmatch[0].rm_eo;
	}
	break;
    case REG_NOMATCH:
	rc = MATCH_NOMATCH;
	break;
    default:
	(void) regerror(rc, &preg, re_errmsg, sizeof(re_errmsg));
	write_log(WRITE_LOG_PANIC, "match_re: error matching '%v' with expression '%v': %s",
		  key, exp, re_errmsg);
	rc = MATCH_FAIL;		/* error during matching */
    }
    regfree(&preg);

    DEBUG3(DBG_DRIVER_HI, "match_re: '%v' %s expression '%v'\n", key, rc ? "matched" : "did not match", exp);

    return rc;
}


/*
 * match_re_list - match a string against a list of REs
 *
 * Takes a list of RE:RE:RE and compares each against the string until a match
 * is found or the list is exhausted.
 *
 * RE is a Regular Expression.
 *
 * RE may optionally be preceded by "![ \t]*" which will reverse the return
 * value (i.e. immediately return 0 instead of 1) if the RE matches.
 *
 * RE may optionally be followed by ';message text', which of course must not
 * contain a ':'
 *
 * WARNING: cannot diddle with "patterns" storage -- may be read-only string.
 *
 * RETURNS:  MATCH_MATCHED if matched, else MATCH_NOMATCH (unless the matching
 * RE was prefixed by '!', in which case it returns the opposite), or
 * MATCH_FAIL if anything goes wrong.
 *
 * if a match is found and if a sub-field accompanies the matching item then
 * the *reasonp will be set to point to a copy of its value too.
 *
 */
int
match_re_list(str, patterns, icase, reasonp)
    char *str;			/* string we are matching */
    char *patterns;		/* list of patterns we are using to match it */
    int icase;			/* ignore case? */
    char **reasonp;		/* optional pointer for returned message string */
{
    size_t str_len = strlen(str);
    char *ent_p;
    char *end_ent;
    char *no_reason;
    
    if (!patterns) {
	DEBUG1(DBG_ADDR_HI, "match_re_list(): null patterns pointer, nothing to do for %v...\n", str);
	return MATCH_NOMATCH;
    }
    DEBUG3(DBG_ADDR_HI, "match_re_list(%v, %v, %s, %s): called...\n", str, patterns, icase ? "icase" : "0");
    if (!reasonp) {
	reasonp = &no_reason;
    }
    *reasonp = NULL;

    /* run through the pattern list */
    for (ent_p = patterns; /* NOTEST */ ; ent_p = end_ent + 1) {
	int matchresult = MATCH_MATCHED;
	char prev_c;

	/* skip any spaces at the beginning */
	while (*ent_p && isspace((int) *ent_p)) {
	    ent_p++;
	}
	if (*ent_p == '\0') {
	    DEBUG(DBG_MAIN_HI, "match_re_list(): skipping empty final entry.\n");
	    break;
	}
	/*
	 * find the end of this entry, i.e. the next unescaped ':'
	 *
	 * Note there are no other internal quoting mechanisms (as there are in
	 * the full PCRE lists) except the backslash so we don't have to worry
	 * about any other quoting delimiter.
	 */
	end_ent = ent_p;
	prev_c = '\0';
	while (*end_ent && (*end_ent != ':' || prev_c == '\\')) {
	    prev_c = *end_ent;
	    end_ent++;
	}
	/* only dig deeper into non-empty entries */
	if (end_ent > ent_p) {
	    char *start_re = ent_p;
	    char *end_re = NULL;
	    static struct str exp;
	    static int exp_inited = FALSE;
	    static struct str msg;
	    static int msg_inited = FALSE;
	    pcre_info_t *re_info;
	    int result;

	    /* check for a negation sign */
	    if (*start_re == '!') {
		start_re++;
		matchresult = MATCH_NOMATCH;	/* a match of a negated item... */
		/* skip any spaces after the '!'  */
		while (*start_re && start_re > end_ent && isspace((int) *start_re)) {
		    start_re++;
		}
	    }
	    /*
	     * search for the end of the RE, i.e. the next un-escaped ';', or
	     * the end of the entry, or the end of the string, whichever comes
	     * first
	     */
	    end_re = start_re;
	    prev_c = '\0';
	    while (*end_re &&
		   (*end_re != ';' || prev_c == '\\') &&
		   end_re < end_ent) {
		prev_c = *end_re;
		end_re++;
	    }
	    if (start_re == end_re) {
		/* skip empty REs */
		DEBUG(DBG_MAIN_HI, "match_re_list(): skipping empty RE\n");
		continue;
	    }
	    if (!exp_inited) {
		STR_INIT(&exp);
		exp_inited = TRUE;
	    } else {
		STR_CHECK(&exp);
		STR_CLEAR(&exp);
	    }
	    STR_NEXT(&exp, '^');	/* create a fully anchored RE pattern  */
	    STR_NCAT(&exp, start_re, (size_t) (end_re - start_re));
	    STR_CAT(&exp, "$");		/* NUL-terminates the string too */

	    if (!msg_inited) {
		STR_INIT(&msg);
		msg_inited = TRUE;
	    } else {
		STR_CHECK(&msg);
		STR_CLEAR(&msg);
	    }
	    if (*end_re == ';') {
		/* note we're skipping the ';' itself */
		STR_NCAT(&msg, end_re + 1, (size_t) (end_ent - end_re - 1));
		STR_NEXT(&msg, '\0');
	    }
	    /* RE lists can be dynamic so we cannot cache this... */
	    if (!(re_info = compile_pcre_expr(STR(&exp), '/', icase ? "i" : "", STR(&msg), matchresult, reasonp))) {
		/* exp and msg are reused... */
		return MATCH_FAIL;
	    }
	    result = match_pcre_expr(str, str_len, re_info, TRUE, reasonp);
	    /*
	     * note that since we never pre-compile these patterns we could
	     * just use matchresult without resetting it from re_info, but
	     * doing it this way preserves the compile_pcre_expr() API.
	     */
	    matchresult = re_info->matchresult;
	    /* note re_info->{re,msg} are reused and re_info->opts is static */
	    xfree((char *) re_info->code);
	    xfree((char *) re_info);
	    re_info = NULL;

	    switch (result) {
	    case MATCH_NOMATCH:
		/* just continue after the matching so the end-of-list test works! */
		break;
	    case MATCH_MATCHED:
		DEBUG4(DBG_MAIN_MID, "match_re_list(): '%s%v' %s: returning %s\n",
		       matchresult == MATCH_NOMATCH ? "! " : "",
		       str,
		       result == MATCH_MATCHED ? "matched" : "notmatched",
		       matchresult == MATCH_MATCHED ? "matched" : "NOT-matched");
		return matchresult;
	    case MATCH_FAIL:
		DEBUG1(DBG_MAIN_MID, "match_re_list(): error: %s\n", *reasonp);
		return MATCH_FAIL;
#if 0 /* #ifndef NDEBUG */
	    default:
		assert(FALSE);
#endif
	    }
	    /* continue with next pattern */
	} else {
	    DEBUG(DBG_MAIN_HI, "match_re_list(): skipping empty list entry\n");
	}
	if (*end_ent == '\0') {
	    break;				/* if there is one */
	}
    }
    DEBUG2(DBG_ADDR_HI, "string '%v' does not match in '%v'\n",
	   str, patterns);
    
    return MATCH_NOMATCH;
}


#ifdef HAVE_BSD_NETWORKING

/*
 * match_ip - match an ip against a list of host/net numbers
 *
 * Takes a list of IP:IP:IP and compares against an IP given in ASCII.
 *
 * 'IP' is an IP number or IP/bits CIDR network spec.
 *
 * IP may optionally be preceded by "![ \t]*" which will reverse the return
 * value (i.e. immediately return 0 instead of 1) if the IP matches.
 *
 * IP may optionally be followed by ';message text', which of course must not
 * contain a ':'
 *
 * Note that the field separator shown above as ':' is specified by the caller,
 * as is message sub-field separator show above as ';'.  Normally the field
 * separator will be either ':' or ',' and the sub-field separator will be ';'
 * or '\0' (i.e. none).
 *
 * WARNING:  cannot diddle with pattern -- may be read-only string.
 *
 * RETURNS:  TRUE if matched, else FALSE (unless the IP was prefixed by '!', in
 * which case it returns the opposite).
 *
 * WARNING:  does not use the MATCH_* returns -- callers only expect boolean.
 *
 * If a match is found and if a sub-field accompanies the matching item then
 * the *reasonp will be set to point to a copy of its value too.
 *
 * WARNING:  There is no error reporting capability in this function's API.  If
 * the pattern is mal-formed then in theory it can never be matched and in
 * theory if a mal-formed pattern is negated with a '!' then 1 should be
 * returned immediately.  However this is not currently the case since given
 * the way this function is used such behaviour would most likely cause an
 * uninentional wildcard match.
 *
 * XXX allow_wildcard isn't actually used and should be zapped.
 */
unsigned int
match_ip(ip, patterns, fldsep, msgsep, allow_wildcard, reasonp)
    /*const*/ char *ip;		/* ip address we are matching */
    /*const*/ char *patterns;	/* list of patterns we are using to match it */
    int fldsep;			/* field separator character */
    int msgsep;			/* optional message string separator */
    int allow_wildcard;		/* allow 0/0? */
    char **reasonp;		/* optional pointer for returned message string */
{
    /*const*/ char *pat_p;
    /*const*/ char *end_p;
    size_t len;
    
    if (!patterns) {
	DEBUG1(DBG_ADDR_HI, "match_ip(): null patterns pointer, nothing to do for %v...\n", ip);
	return FALSE;
    }
    DEBUG4(DBG_ADDR_HI, "match_ip(%v, %v, %c, %c): called...\n", ip, patterns, fldsep, msgsep);
    if (reasonp) {
	*reasonp = NULL;
    }

    /* run through the pattern list */
    for (pat_p = patterns; /* NOTEST */ ; pat_p = end_p + 1) {
	int matchresult = TRUE;
	char prev_c;

	/* skip any spaces at front */
	while (*pat_p && isspace((int) *pat_p)) {
	    pat_p++;
	}
	if (*pat_p == '\0') {
	    DEBUG(DBG_MAIN_HI, "match_ip(): skipping empty final entry.\n");
	    break;
	}
	/* find the end of this entry */
	end_p = pat_p;
	prev_c = '\0';
	while (*end_p && (*end_p != fldsep || prev_c == '\\')) {
	    prev_c = *end_p;
	    end_p++;
	}
	if (end_p > pat_p && *pat_p == '!') {
	    pat_p++;
	    matchresult = FALSE;	/* a match of a negated item... */
	    /* skip any spaces after the '!'  */
	    while (*pat_p && pat_p > end_p && isspace((int) *pat_p)) {
		pat_p++;
	    }
	}
	/* skip empty patterns */
	/* XXX should really allow empty patterns with "; text"  */
	len = end_p - pat_p;
	if (len > 0) {
	    char *p;
	    static struct str exp;
	    static int inited = FALSE;

	    if (!inited) {
		STR_INIT(&exp);
		inited = TRUE;
	    } else {
		STR_CHECK(&exp);
		STR_CLEAR(&exp);
	    }
	    STR_NCAT(&exp, pat_p, len);
	    STR_NEXT(&exp, '\0');
	    p = NULL;
	    if (msgsep && (p = strchr(STR(&exp), msgsep))) {
		*p = '\0';
	    }
	    /* warn if we have a deprecated wild-card at end of the current pattern */
	    if (*(end_p - 1) == '*') {
		/*
		 * this could get rather noisy for the first time around, but
		 * keeping track of which ones we've warned about, even per
		 * process, is damn near impossible to do safely.
		 */
		write_log(WRITE_LOG_PANIC,
			  "match_ip(): an unsupported wildcard pattern [%v] was found in '%v'!",
			  STR(&exp), patterns);
	    }
	    /* XXX localnet should be deprecated */
	    if (strcmpic("localnet", STR(&exp)) == 0) {
		DEBUG2(DBG_ADDR_HI,
		       "match_ip(): found 'localnet' in list...\n",
		       ip,
		       smtp_local_net);
		/* our pattern will be the string in smtp_local_net */
		if (!smtp_local_net) {
		   DEBUG1(DBG_ADDR_HI, "match_ip(): nil smtp_local_net cannot match %v...\n", ip);
		   /* continue with next pattern */
		} else {
		    switch (match_ip_net(ip, smtp_local_net, allow_wildcard)) {
		    case MATCH_NOMATCH:
			break;		/* not this one! */
		    case MATCH_FAIL:
			/* this should be almost impossible for smtp_local_net */
			write_log(WRITE_LOG_PANIC,
				  "match_ip(): invalid smtp_local_net [%v] was found for localnet in '%v'!",
				  smtp_local_net, patterns);
			break;
		    default:
			if (p && reasonp) {
			    *reasonp = COPY_STRING(p + 1);
			}
			DEBUG5(DBG_ADDR_MID,
			       "ip [%v] matches %slocalnet [%v]%s%v\n",
			       ip,
			       matchresult ? "" : "!",
			       smtp_local_net,
			       (reasonp && *reasonp) ? " with comment: " : "",
			       (reasonp && *reasonp) ? *reasonp : "");
			return matchresult;
		    }
		}
		/* continue with next pattern */
#ifdef HAVE_LIBWHOSON
	    } else if (strcmpic("whoson", STR(&exp)) == 0) {
		int wso;
		char retbuf[BUFSIZ];

		DEBUG1(DBG_ADDR_HI,
		       "(checking if [%v] is listed in the 'whoson' database)\n", ip);
		*retbuf = '\0';
		wso = wso_query(ip, retbuf, sizeof(retbuf));
		retbuf[sizeof(retbuf) - 1] = '\0'; /* just in case... */
		if (wso < 0 && *retbuf) {
		    /* XXX hope this doesn't flood the log! */
		    write_log(WRITE_LOG_PANIC, "wso_query(%v): whoson query failed: %v", ip, retbuf);
		    /* continue with next pattern */
		} else if (wso == 0) {
		    DEBUG3(DBG_ADDR_MID, "ip [%v] found by whoson: %s%v.\n",
			   ip,
			   matchresult ? "" : "!",
			   retbuf);
		    return matchresult;
		} else {
		    DEBUG1(DBG_ADDR_MID, "ip [%v] NOT found by whoson\n", ip);
		    /* continue with next pattern */
		}
#endif
	    } else {
		switch (match_ip_net(ip, STR(&exp), allow_wildcard)) {
		case MATCH_NOMATCH:
		    break;	/* not this one! */
		case MATCH_FAIL:
		    write_log(WRITE_LOG_PANIC,
			      "match_ip(): invalid netspec [%v] was found in '%v'!",
			      STR(&exp), patterns);
		    break;
		default:
		    if (p && reasonp) {
			*reasonp = COPY_STRING(p + 1);
		    }
		    DEBUG5(DBG_ADDR_MID,
			   "ip [%v] matches IP/network of %s[%v]%s%v\n",
			   ip,
			   matchresult ? "" : "!",
			   STR(&exp),
			   (reasonp && *reasonp) ? " with comment: " : "",
			   (reasonp && *reasonp) ? *reasonp : "");
		    return matchresult;
		}
		/* continue with next pattern */
	    }
	}
	if (*end_p == '\0') {
	    break;
	}
    }

    DEBUG2(DBG_ADDR_HI, "match_ip(): [%v] does not match any IP/network in '%v'\n",
	   ip, patterns);
    
    return FALSE;
}

/*
 * RETURNS:  MATCH_MATCHED, MATCH_NOMATCH, or MATCH_FAIL on error.
 *
 * XXX allow_wildcard isn't actually used and should be zapped.
 */
int
match_ip_net(ipaddr, netspec, allow_wildcard)
    char *ipaddr;
    char *netspec;
    int allow_wildcard;
{
    in_addr_t dest;
    in_addr_t net;
    in_addr_t mask;
    int bits = 0;

    DEBUG2(DBG_ADDR_HI, "match_ip_net([%v], [%v]) called...\n", ipaddr, netspec);

#if 1 /* HAVE_INET_NET_PTON */
    net = 0;			/* net must be pre-initialized */
    if ((bits = inet_net_pton(AF_INET, netspec, (void *) &net, sizeof(net))) == -1) {
	DEBUG2(DBG_ADDR_LO, "match_ip_net(): [%v] is invalid: %s\n", netspec, strerror(errno));
	return MATCH_FAIL;
    }
#else  /* BOGUS OLD CODE: */
    char *slash;

    /*
     * inet_net_pton() is a lot smarter than this, properly handling inet(3)
     * numbers in their full IPV4 (and IPV6) formats.
     */
    slash = strchr(netspec, '/');
    if (slash) {
	netspec = COPY_STRING(netspec);
	slash = strchr(netspec, '/');		/* find again in copy */
	*slash = '\0';
	bits = atoi(slash + 1);
    } else {
	bits = 0;				/* compare whole addr. (XXX dumb) */
    }
    if ((net = inet_addr(netspec)) == INADDR_NONE) {
	DEBUG1(DBG_ADDR_LO, "match_ip_net(): [%v] is invalid\n", netspec);
	return MATCH_FAIL;			/* malformed address */
    }
    if (slash) {
	xfree(netspec);
    }
#endif
    if (bits > 32 || bits < 0) {
	DEBUG2(DBG_ADDR_LO, "match_ip_net(): [%v] has invalid number of bits: %d\n", netspec, bits);
	return MATCH_FAIL;
    }
    net = ntohl(net);
    if (!bits && net) {
	DEBUG1(DBG_ADDR_LO, "match_ip_net(): wildcard [%v] has non-zero bits in network part\n", netspec);
	return MATCH_FAIL;
    }
    if (!net && !allow_wildcard) {	/* all zero network bits is a wildcard */
	DEBUG1(DBG_ADDR_LO, "match_ip_net(): [%s] has all zero network bits.\n", netspec);
	return MATCH_FAIL;
    }
    if (!bits && !allow_wildcard) {	/* zero bits is a wildcard */
	DEBUG1(DBG_ADDR_LO, "match_ip_net(): [%s] has zero network bits.\n", netspec);
	return MATCH_FAIL;
    }
    /* 
     * Note: ((u_int32_t) << 32) is undefined according to the ANSI C spec.
     * Hence the extra test for bits == 0.
     */
    mask = (bits == 0) ? 0 : (0xffffffffL << (32 - bits));

    dest = 0;
    switch (inet_pton(AF_INET, ipaddr, &dest)) {
    case 0:
	DEBUG1(DBG_ADDR_LO, "match_ip_net(): [%s] is not parsable.\n", ipaddr);
	return MATCH_FAIL;
    case -1:
	DEBUG2(DBG_ADDR_LO, "match_ip_net(): [%s] is invalid: %s\n", ipaddr, strerror(errno));
	return MATCH_FAIL;
    }
    dest = ntohl(dest);
#if 0 /* noisy!  for really heavy duty debugging only! */
    DEBUG4(DBG_ADDR_HI, "match_ip_net(): dest=0x%x, net=0x%x, bits=%d, mask=0x%x\n", dest, net, bits, mask);
#endif
    if (mask && (net & ~mask)) {
	DEBUG2(DBG_ADDR_MID, "match_ip_net(): warning: [%v] has non-zero bits in host part (0x%x).\n", netspec, net & ~mask);
    }

    return ((dest & mask) == (net & mask));
}

#define CHOP_B(x)	((int) ((x) & 0xff))

/*
 * flip the octets of an Internet address, e.g. to create an in-addr.arpa name
 */
char *
flip_inet_addr(inaddr)
    char *inaddr;
{
    in_addr_t addr;

    addr = get_inet_addr(inaddr);

    addr = ntohl(addr);

    return xprintf("%u.%u.%u.%u",
		   CHOP_B(addr),
		   CHOP_B(addr >> 8),
		   CHOP_B(addr >> 16),
		   CHOP_B(addr >> 24));
}

#undef CHOP_B

#endif /* HAVE_BSD_NETWORKING */

/*
 * compile and study the REs in re_list producing a charplist_t
 */
charplist_t *
compile_pcre_list(re_list, errorp)
    char *re_list;
    char **errorp;
{
    charplist_t *nl = NULL;
    charplist_t *cur;
    charplist_t *next;
    charplist_t *new;
    /*const*/ char *ent_p;
    /*const*/ char *end_ent;

    *errorp = NULL;

    if (! re_list) {
	return NULL;
    }

    /* run through the pattern list */
    for (ent_p = re_list; /* NOTEST */ ; ent_p = end_ent + 1) {
	char prev_c;
	int delim_c;
	/*const*/ char *start_re;
	/*const*/ char *end_re = NULL;
	char *p;
	struct str exp;
	struct str opts;
	struct str msg;
	size_t msglen;
	pcre_info_t *re_info;
	int matchresult;

	/* skip any spaces at the beginning */
	while (*ent_p && isspace((int) *ent_p)) {
	    ent_p++;
	}
	if (*ent_p == '\0') {
	    DEBUG(DBG_MAIN_HI, "compile_pcre_list(): skipping empty final entry.\n");
	    break;
	}
	/*
	 * the first thing in an entry is the RE
	 */
	start_re = end_ent = ent_p;
	/*
	 * unless it is the standard '!' negation character, in which case the
	 * RE starts at the next non-space char....
	 */
	if (*start_re == '!') {
	    start_re++;
	    matchresult = MATCH_NOMATCH;	/* a match of a negated item... */
	    /* skip any spaces after the '!' */
	    while (*start_re && isspace((int) *start_re)) {
		start_re++;
	    }
	} else {
	    matchresult = MATCH_MATCHED;
	}
	/*
	 * the first non-space char of an RE is the RE delimiter char
	 */
	delim_c = *start_re++;
	if (delim_c == ':') {
	    DEBUG(DBG_MAIN_HI, "compile_pcre_list(): skipping empty entry.\n");
	    continue;			/* XXX is this always safe? */
	}
	if (delim_c == ';') {
	    *errorp = xprintf("';' is not a valid RE delimiter");
	    return NULL;
	}
	/*
	 * walk the entry end pointer up to the end of the RE -- i.e. the next
	 * unescaped delim_c
	 */
	end_ent = start_re + 1;
	if (*end_ent == '\0') {
	    *errorp = xprintf("list ends before last RE's closing '%c' delimiter",
			      delim_c);
	    return NULL;
	}
	prev_c = '\0';
	while (*end_ent && (*end_ent != delim_c || prev_c == '\\')) {
	    prev_c = *end_ent;
	    end_ent++;
	}
	end_re = end_ent;
	if (*end_re != delim_c) {
	    *errorp = xprintf("RE '%S' has no closing '%c' delimiter",
			      (size_t) (end_ent - ent_p), ent_p,
			      delim_c);
	    return NULL;
	}
	/*
	 * walk the the end pointer past any options and message text up to the
	 * beginning of the next list entry, i.e. the next un-escaped ':'
	 */
	prev_c = '\0';
	while (*end_ent && (*end_ent != ':' || prev_c == '\\')) {
	    prev_c = *end_ent;
	    end_ent++;
	}

	/* don't do this check until after end_ent points to any next entry */
	if (start_re == end_re) {
	    /* skip empty patterns */
	    DEBUG(DBG_MAIN_HI, "compile_pcre_list(): skipping empty RE\n");
	    /* this should be safe */
	    continue;
	}
	STR_INIT(&exp);			/* do not re-use */
	STR_NCAT(&exp, start_re, (size_t) (end_re - start_re));
	STR_NEXT(&exp, '\0');		/* NUL-terminates the string */

	/* point past the final delimiter again */
	end_re++;

	/* get (optional) options:  "/RE/options" */
	for (p = end_re; p <= end_ent && *p && *p != ';' && *p != ':'; p++) {
	    ; /* NO-OP */
	}
	if (p > end_re) {
	    STR_INIT(&opts);		/* do not re-use */
	    STR_NCAT(&opts, end_re, (size_t) (p - end_re));
	    STR_NEXT(&opts, '\0');
	} else {
	    STR_ZAP(&opts);		/* do not STR_FREE() any previous one! */
	}

	/* get (optional) message:  "/RE/options; message" */
	msglen = end_ent - p - 1;
	if (*p++ == ';' && msglen > 0) {
	    STR_INIT(&msg);		/* do not re-use */
	    STR_NCAT(&msg, p, msglen);
	    STR_NEXT(&msg, '\0');
	} else {
	    STR_ZAP(&msg);		/* do not STR_FREE() any previous one! */
	}
	if (!(re_info = compile_pcre_expr(STR(&exp), delim_c, STR(&opts), STR(&msg), matchresult, errorp))) {
	    STR_FREE(&exp);
	    STR_FREE(&opts);
	    STR_FREE(&msg);
	    free_pcre_list(nl);
	    return NULL;
	}
	nl = add_charplist(nl, (char *) re_info); /* XXX use a (void *) list? */

	/* continue with next pattern */
	if (*end_ent == '\0') {
	    break;			/* if there is one */
	}
    }
    /* finally reverse the list so tests happen in the stated order */
    /* XXX could implement and use append_charplist() above to avoid this */
    new = NULL;
    for (cur = nl; cur; cur = next) {
	next = cur->succ;
	cur->succ = new;
	new = cur;
    }

    return new;
}

static pcre_info_t *
compile_pcre_expr(exp, delim_c, opts, msg, matchresult, errorp)
    char *exp;
    int delim_c;
    char *opts;
    char *msg;
    int matchresult;
    char **errorp;
{
    pcre_info_t *re_info;
    int capturecount;
    const char *errtxt;
    int err_offset;
    int rc;

    re_info = (pcre_info_t *) xmalloc(sizeof(*re_info));
    re_info->re = exp;
    re_info->delim_c = delim_c;
    re_info->matchresult = matchresult;
    re_info->flags = 0;

    if (opts) {
	const char *o;

	re_info->opts = opts;

	for (o = re_info->opts; *o; o++) {
	    switch (*o) {
	    case 'A':
		re_info->flags |= PCRE_ANCHORED;
		continue;
	    case 'E':	/* XXX PHP-4.x uses 'D' */
		re_info->flags |= PCRE_DOLLAR_ENDONLY;
		continue;
	    case 'U':
		re_info->flags |= PCRE_UNGREEDY;
		continue;
	    case 'X':
		re_info->flags |= PCRE_EXTRA;
		continue;
	    case 'i':
		re_info->flags |= PCRE_CASELESS;
		continue;
	    case 'm':
		re_info->flags |= PCRE_MULTILINE;
		continue;
	    case 's':
		re_info->flags |= PCRE_DOTALL;
		continue;
	    case 'x':
		re_info->flags |= PCRE_EXTENDED;
		continue;
	    }
	    if (! isspace((int) *o)) {
		*errorp = xprintf("invalid option '%c' for RE %c%v%c%v",
				  *o, delim_c, re_info->re, delim_c, re_info->opts);
		xfree(re_info->re);
		xfree(re_info->opts);
		xfree((char *) re_info);
		return NULL;
	    }
	}
    } else {
	re_info->opts = NULL;
    }
    /* get any accompanying message into re_info->msg */
    re_info->msg = msg;

    /* note: we use %s here so we see the RE in it's "natural" form. */
    DEBUG4(DBG_MAIN_HI, "compile_pcre_expr(): compiling RE %c%v%c%v\n",
	   delim_c, re_info->re, delim_c, re_info->opts ? re_info->opts : "");
    if (re_info->msg) {
	DEBUG1(DBG_MAIN_HI, "\twith message '%v'\n", re_info->msg);
    }
    re_info->code = pcre_compile(re_info->re, re_info->flags, &errtxt, &err_offset, (const unsigned char *) NULL);
    if (! re_info->code) {
	*errorp = xprintf("error in regex %c%v%c%v at offset %d: %s",
			  delim_c, re_info->re, delim_c,
			  re_info->opts ? re_info->opts : "",
			  err_offset, errtxt);
	xfree((char *) re_info);
	return NULL;
    }
    errtxt = NULL;
    re_info->hints = pcre_study(re_info->code, 0, &errtxt);
    if (re_info->hints) {
	DEBUG1(DBG_MAIN_HI, "compile_pcre_expr(): pcre_study() returned optimisation stuff:  match_limit=%lu\n", re_info->hints->match_limit);
    }
    if (!re_info->hints && errtxt) {
	*errorp = xprintf("error while studying RE %c%v%c%v: %s",
			  delim_c, re_info->re, delim_c,
			  re_info->opts ? re_info->opts : "",
			  errtxt);
	xfree((char *) re_info->code);
	xfree((char *) re_info);
	return NULL;
    }
    if ((rc = pcre_fullinfo(re_info->code,
			    re_info->hints,
			    PCRE_INFO_CAPTURECOUNT,
			    &capturecount)) != 0) {
	*errorp = xprintf("pcre_fullinfo(CAPTURECOUNT) failed [%d] for RE %c%v%c%v",
			  rc,
			  delim_c, re_info->re, delim_c,
			  re_info->opts ? re_info->opts : "");
	xfree((char *) re_info->code);
	xfree((char *) re_info);
	return NULL;
    }
    /* we always capture at least one string -- the whole match... */
    capturecount++;
    re_info->ovecsize = capturecount * 3;	/* table is 3 wide */
   
    return re_info;
}

/*
 * match_pcre_list - match the string sp against a pre-compiled list of PCRE REs
 *
 * RETURNS:  MATCH_MATCHED if matched, else MATCH_NOMATCH, and MATCH_FAIL if
 * anything goes wrong.
 *
 * May return MATCH_NOMATCH immediately if a negated expression matches, thus
 * terminating the search.
 *
 * If a match is found, even a negated match, and if re_info->msg is not NULL,
 * then the *reasonp will be set to point to a copy of its value, after
 * sub-string substitutions have been performed on it if do_subs is non-zero.
 *
 * If no match is found and no errors encountered, or if re_info->msg is NULL,
 * then *reasonp will also be set to NULL.
 *
 * XXX Maybe "sp" should just be a "struct str *" and get rid of len?
 */
int
match_pcre_list(sp, len, pcre_list, do_subs, reasonp)
    char *sp;			/* string we are matching */
    size_t len;			/* length of string */
    charplist_t *pcre_list;	/* pre-compiled RE list */
    int do_subs;		/* perform substitutions on message? */
    char **reasonp;		/* optional pointer for returned message string */
{
    charplist_t *pat;
    char *no_reason;

    if (!reasonp) {
	reasonp = &no_reason;
    }
    *reasonp = NULL;

    for (pat = pcre_list; pat; pat = pat->succ) {
	pcre_info_t *re_info = (pcre_info_t *) pat->text;

	switch (match_pcre_expr(sp, len, re_info, do_subs, reasonp)) {
	case MATCH_NOMATCH:
	    continue;
	case MATCH_MATCHED:
	    return re_info->matchresult;
	case MATCH_FAIL:
	    return MATCH_FAIL;
#if 0 /* #ifndef NDEBUG */
	default:
	    assert(FALSE);
#endif
	}
    }

    return MATCH_NOMATCH;
}

/*
 * match_pcre_expr() - match an RE compiled by compile_pcre_expr()
 *
 * RETURNS:  MATCH_MATCHED if matched, else MATCH_NOMATCH, and MATCH_FAIL if
 * anything goes wrong.
 *
 * Note re_info->matchresult is ignored -- it is only useful in the context of
 * a list of REs and so it can only sensibly be made use of by the caller.
 *
 * If a match is found and if re_info->msg is not NULL, then the *reasonp will
 * be set to point to a copy of its value, after sub-string substitutions have
 * been performed on it if do_subs is non-zero.
 *
 * If returning MATCH_FAIL then *reasonp will point to an error message;
 *
 * XXX Maybe "sp" should just be a "struct str *" and get rid of len?
 */
static int
match_pcre_expr(sp, len, re_info, do_subs, reasonp)
    char *sp;			/* string we are matching */
    size_t len;			/* length of string */
    pcre_info_t *re_info;	/* RE stuff [result of compile_pcre_expr()] */
    int do_subs;		/* perform substitutions on message? */
    char **reasonp;		/* pointer for returned message string */
{
    int rc;
    int *ovec;

    /* XXX using DBG_MAIN_* is a hack... should have DBG_MATCH_* */
    DEBUG6(DBG_MAIN_MID, "match_pcre_expr(): checking RE %s%c%v%c%v\n\tagainst '%v'\n",
	   re_info->matchresult == MATCH_NOMATCH ? "! " : "",
	   re_info->delim_c, re_info->re, re_info->delim_c,
	   re_info->opts ? re_info->opts : "",
	   (debug >= DBG_MAIN_HI) ? sp : "(not shown)");

    ovec = (int *) xmalloc((size_t) re_info->ovecsize * sizeof(int));
    rc = pcre_exec(re_info->code, re_info->hints, sp, (int) len, 0, 0, ovec, re_info->ovecsize);
    if (rc == PCRE_ERROR_NOMATCH) {
	xfree((char *) ovec);

	return MATCH_NOMATCH;
    }
    if (rc <= 0) {
	switch (rc) {
	case 0:
	    *reasonp = xprintf("pcre_exec() encountered too many subexpressions in RE %c%v%c%v (%d expected)",
			       re_info->delim_c, re_info->re, re_info->delim_c,
			       re_info->opts ? re_info->opts : "",
			       ((re_info->ovecsize / 3) - 1));
	    break;
	case PCRE_ERROR_NULL:
	    /* XXX should we print 'sp' here too?  only if really null? */
	    *reasonp = xprintf("bad pcre_exec() parameter for RE %c%v%c%v",
			       re_info->delim_c, re_info->re, re_info->delim_c,
			       re_info->opts ? re_info->opts : "");
	    break;
	case PCRE_ERROR_NOMEMORY: /* XXX impossible */
	    *reasonp = xprintf("pcre_exec() ran out of memory while matching RE %c%v%c%v",
			       re_info->delim_c, re_info->re, re_info->delim_c,
			       re_info->opts ? re_info->opts : "");
	    break;
	case PCRE_ERROR_MATCHLIMIT: {
	    int ml;

	    (void) pcre_config(PCRE_CONFIG_MATCH_LIMIT, (void *) &ml);
	    *reasonp = xprintf("pcre_exec() hit recursion or backtracking limit using RE %c%v%c%v (%smatch limit = %d)",
			       re_info->delim_c, re_info->re, re_info->delim_c,
			       re_info->opts ? re_info->opts : "",
			       re_info->hints ? "using default " : "",
			       re_info->hints ? re_info->hints->match_limit : ml);
	    break;
	}
	case PCRE_ERROR_BADMAGIC:
	case PCRE_ERROR_UNKNOWN_NODE:
	    *reasonp = xprintf("pcre_exec() given corrupt compiled RE %c%v%c%v",
			       re_info->delim_c, re_info->re, re_info->delim_c,
			       re_info->opts ? re_info->opts : "");
	    break;
	default:
	    *reasonp = xprintf("unexpected pcre_exec() error code #%d using RE %c%v%c%v",
			       rc,
			       re_info->delim_c, re_info->re, re_info->delim_c,
			       re_info->opts ? re_info->opts : "");
	    break;
	}
	DEBUG1(DBG_MAIN_LO, "match_pcre_expr(): %s\n", *reasonp);
	xfree((char *) ovec);

	return MATCH_FAIL;
    }
    DEBUG6(DBG_MAIN_HI, "match_pcre_expr(): matched RE %s%c%v%c%v\n\twith %d substrings\n",
	   re_info->matchresult == MATCH_NOMATCH ? "! " : "",
	   re_info->delim_c, re_info->re, re_info->delim_c, re_info->opts,
	   rc);
    if (re_info->msg) {
	if (do_subs) {
	    *reasonp = pcre_expand_msg(re_info, sp, ovec, rc);
	} else {
	    *reasonp = xprintf("%v", re_info->msg);
	}
    } else {
	const char *matched = NULL;
	
	if ((rc = pcre_get_substring(sp, ovec, rc, 0, &matched)) < 0) {
	    DEBUG5(DBG_MAIN_LO, "match_pcre_expr():  unexpected error #%d getting matched string for RE %c%v%c%v\n",
		   rc,
		   re_info->delim_c, re_info->re, re_info->delim_c, re_info->opts);
	    matched = sp;
	}
	*reasonp = xprintf("RE %c%v%c%v matched '%v'",
			   re_info->delim_c, re_info->re, re_info->delim_c,
			   re_info->opts ? re_info->opts : "",
			   matched);
	if (rc >= 0) {
	    pcre_free_substring(matched);
	}
    }
    xfree((char *) ovec);

    return MATCH_MATCHED;
}

/*
 * Do Python-style variable substitution on the message associated with the RE
 * using values matched by the named subpatterns.
 */
/* ARGSUSED */
static char *
pcre_expand_msg(re_info, sp, ovec, nss)
    pcre_info_t *re_info;
    char *sp;			/* len not needed for pcre_get_*substring() */
    int *ovec;
    int nss;
{
    struct str out;
    struct str *op = &out;
    char *p;			/* message index pointer */
    char *q;			/* digit pointer */
    char *ep;			/* message end pointer */
    char *np;			/* substring name */
    const char *vp;		/* substring value pointer */
    int vlen;			/* substring value length */

    STR_INIT(op);

    for (p = re_info->msg; *p; p++) {
	if (*p == '$' || *p == '%') {
	    p++;
	    if (*p == '{') {
		p++;
		for (ep = p; *ep && *ep != '}' && (isalnum(*ep) || *ep == '_'); ep++) {
		    ; /* NO-OP */
		}
		if (*ep != '}') {
		    DEBUG3(DBG_MAIN_LO, "pcre_expand_msg():  unterminated variable expression: '${%S' in '%v'\n",
			   (size_t) (ep - p), p,
			   re_info->msg);
		    /* just put back the expression */
		    STR_CAT(op, "${");
		    STR_NCAT(op, p, (size_t) (ep - p));
		    STR_NEXT(op, '}');
		    p = ep;
		    continue;
		}
	    } else {
		for (ep = p; *ep && (isalnum(*ep) || *ep == '_'); ep++) {
		    ; /* NO-OP */
		}
	    }
	    np = strncpy(xmalloc((size_t) (ep - p + 1)), p, (size_t) (ep - p));
	    np[ep - p] = '\0';
	    for (q = np; isdigit(*q); q++) {
		; /* NO-OP */
	    }
	    vp = NULL;
	    if (*q == '\0') {
		vlen = pcre_get_substring(sp, ovec, nss, atoi(np), &vp);
	    } else {
		vlen = pcre_get_named_substring(re_info->code, sp, ovec, nss, np, &vp);
	    }
	    if (vlen < 0) {
		switch (vlen) {
		case PCRE_ERROR_NOSUBSTRING:
		    DEBUG2(DBG_MAIN_LO, "pcre_expand_msg():  no valid substring %s '${%s}'\n",
			   *q ? "named" : "numbered", np);
		    break;
		default:
		    DEBUG7(DBG_MAIN_LO, "pcre_expand_msg():  unexpected error #%d getting substring %s '${%s}' for RE %c%v%c%v\n",
			   vlen,
			   *q ? "named" : "numbered",
			   np,
			   re_info->delim_c, re_info->re, re_info->delim_c, re_info->opts);
		    break;
		}
		/* substitute the empty string... */
	    } else {
		str_printf(op, "%V", (size_t) vlen, vp);
		pcre_free_substring(vp);
	    }
	    xfree(np);
	    p = (*ep == '}') ? ep : (ep - 1);
	} else {
	    STR_NEXT(op, *p);
	}
    }
    STR_NEXT(op, '\0');

    return STR(op);
}

void
free_pcre_list(re_list)
    charplist_t *re_list;
{
    charplist_t *pat;
    charplist_t *next;

    for (pat = re_list; pat; pat = next) {
	pcre_info_t *re_info = (pcre_info_t *) pat->text;

	next = pat->succ;
	xfree((char *) re_info->re);
	xfree((char *) re_info->code);
	if (re_info->opts) {
	    xfree((char *) re_info->opts);
	}
	if (re_info->msg) {
	    xfree((char *) re_info->msg);
	}
	xfree((char *) re_info);
	xfree((char *) pat);
    }
    return;
}

/*
 * turn re_list back into super-safe canonical pretty-printed input form
 */
char *
format_pcre_list(re_list)
    charplist_t *re_list;
{
    struct str fmt;
    struct str *fmtp = &fmt;
    charplist_t *pat;

    STR_INIT(fmtp);
    STR_CAT(fmtp, "\"\n\t:");
    for (pat = re_list; pat; pat = pat->succ) {
	pcre_info_t *re_info = (pcre_info_t *) pat->text;

	STR_NEXT(fmtp, re_info->delim_c);
	/* XXX we could wrap with an escaped newline at the special character
	 * nearest the 80'th column.... */
	str_printf(fmtp, "%v", re_info->re);
	STR_NEXT(fmtp, re_info->delim_c);
	if (re_info->opts) {
	    STR_CAT(fmtp, re_info->opts);
	}
	STR_CAT(fmtp, ";\\\n\t\t");
	if (re_info->msg) {
	    /* XXX we could wrap with an escaped newline at the whitespace
	     * nearest the 80'th column.... */
	    str_printf(fmtp, "%v", re_info->msg);
	}
	STR_CAT(fmtp, "\\\n\t:");
    }
    STR_CAT(fmtp, "\"");

    return STR(fmtp);
}

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