Home | History | Annotate | Line # | Download | only in util
      1 /*	$NetBSD: mac_expand.c,v 1.5 2026/05/09 18:49:22 christos Exp $	*/
      2 
      3 /*++
      4 /* NAME
      5 /*	mac_expand 3
      6 /* SUMMARY
      7 /*	attribute expansion
      8 /* SYNOPSIS
      9 /*	#include <mac_expand.h>
     10 /*
     11 /*	int	mac_expand7(result, pattern, flags, filter, lookup,
     12 /*				dont_expand, context)
     13 /*	VSTRING *result;
     14 /*	const char *pattern;
     15 /*	int	flags;
     16 /*	const char *filter;
     17 /*	const char *lookup(const char *key, int mode, void *context)
     18 /*	const bool dont_parse(const char *key, void *context)
     19 /*	void *context;
     20 /* AUXILIARY FUNCTIONS
     21 /*	int	mac_expand(result, pattern, flags, filter, lookup, context)
     22 /*	VSTRING *result;
     23 /*	const char *pattern;
     24 /*	int	flags;
     25 /*	const char *filter;
     26 /*	const char *lookup(const char *key, int mode, void *context)
     27 /*	void *context;
     28 /*
     29 /*	typedef	MAC_EXP_OP_RES (*MAC_EXPAND_RELOP_FN) (
     30 /*	const char *left,
     31 /*	int	tok_val,
     32 /*	const char *rite)
     33 /*
     34 /*	void	mac_expand_add_relop(
     35 /*	int	*tok_list,
     36 /*	const char *suffix,
     37 /*	MAC_EXPAND_RELOP_FN relop_eval)
     38 /*
     39 /*	MAC_EXP_OP_RES mac_exp_op_res_bool[2];
     40 /
     41 /*	typedef int (*MAC_EXPAND_NAMED_FN) (
     42 /*	VSTRING *out,
     43 /*	const char *arg)
     44 /*
     45 /*	void	mac_expand_add_named_fn(
     46 /*	const char *name,
     47 /*	MAC_EXPAND_NAMED_FN action)
     48 /* DESCRIPTION
     49 /*	This module implements parameter-less named attribute
     50 /*	expansions, both conditional and unconditional. As of Postfix
     51 /*	3.0 this code supports relational expression evaluation.
     52 /*
     53 /*	In this text, an attribute is considered "undefined" when its value
     54 /*	is a null pointer.  Otherwise, the attribute is considered "defined"
     55 /*	and is expected to have as value a null-terminated string.
     56 /*
     57 /*	In the text below, the legacy form $(...) is equivalent to
     58 /*	${...}. The legacy form $(...) may eventually disappear
     59 /*	from documentation. In the text below, the name in $name
     60 /*	and ${name...} must contain only characters from the set
     61 /*	[a-zA-Z0-9_].
     62 /*
     63 /*	The following substitutions are supported:
     64 /* .IP "$name, ${name}"
     65 /*	Unconditional attribute-based substitution. The result is the
     66 /*	named attribute value (empty if the attribute is not defined)
     67 /*	after optional further named attribute substitution.
     68 /* .IP "${name?text}, ${name?{text}}"
     69 /*	Conditional attribute-based substitution. If the named attribute
     70 /*	value is non-empty, the result is the given text, after
     71 /*	named attribute expansion and relational expression evaluation.
     72 /*	Otherwise, the result is empty.  Whitespace before or after
     73 /*	{text} is ignored.
     74 /* .IP "${name{text}}"
     75 /*	Apply the registered function \fIname\fR to the specified text
     76 /*	after named attribute expansion and expression evaluation,
     77 /*	and replace this input with the function result. Functions are
     78 /*	registered with mac_expand_add_named_fn().
     79 /* .IP "${name:text}, ${name:{text}}"
     80 /*	Conditional attribute-based substitution. If the attribute
     81 /*	value is empty or undefined, the expansion is the given
     82 /*	text, after named attribute expansion and relational expression
     83 /*	evaluation.  Otherwise, the result is empty.  Whitespace
     84 /*	before or after {text} is ignored.
     85 /* .IP "${name?{text1}:{text2}}, ${name?{text1}:text2}"
     86 /*	Conditional attribute-based substitution. If the named attribute
     87 /*	value is non-empty, the result is text1.  Otherwise, the
     88 /*	result is text2. In both cases the result is subject to
     89 /*	named attribute expansion and relational expression evaluation.
     90 /*	Whitespace before or after {text1} or {text2} is ignored.
     91 /* .IP "${{text1} == ${text2} ? {text3} : {text4}}"
     92 /*	Relational expression-based substitution.  First, the content
     93 /*	of {text1} and ${text2} is subjected to named attribute and
     94 /*	relational expression-based substitution.  Next, the relational
     95 /*	expression is evaluated. If it evaluates to "true", the
     96 /*	result is the content of {text3}, otherwise it is the content
     97 /*	of {text4}, after named attribute and relational expression-based
     98 /*	substitution. In addition to ==, this supports !=, <, <=,
     99 /*	>=, and >. Comparisons are numerical when both operands are
    100 /*	all digits, otherwise the comparisons are lexicographical.
    101 /*
    102 /*	Arguments:
    103 /* .IP result
    104 /*	Storage for the result of expansion. By default, the result
    105 /*	is truncated upon entry.
    106 /* .IP pattern
    107 /*	The string to be expanded.
    108 /* .IP flags
    109 /*	Bit-wise OR of zero or more of the following:
    110 /* .RS
    111 /* .IP MAC_EXP_FLAG_RECURSE
    112 /*	Expand attributes in lookup results. This should never be
    113 /*	done with data whose origin is untrusted.
    114 /* .IP MAC_EXP_FLAG_APPEND
    115 /*	Append text to the result buffer without truncating it.
    116 /* .IP MAC_EXP_FLAG_SCAN
    117 /*	Scan the input for named attributes, including named
    118 /*	attributes in all conditional result values.  Do not expand
    119 /*	named attributes, and do not truncate or write to the result
    120 /*	argument.
    121 /* .IP MAC_EXP_FLAG_PRINTABLE
    122 /*	Use the printable() function instead of \fIfilter\fR.
    123 /* .PP
    124 /*	The constant MAC_EXP_FLAG_NONE specifies a manifest null value.
    125 /* .RE
    126 /* .IP filter
    127 /*	A null pointer, or a null-terminated array of characters that
    128 /*	are allowed to appear in an expansion. Illegal characters are
    129 /*	replaced by underscores.
    130 /* .IP lookup
    131 /*	The attribute lookup routine. Arguments are: the attribute name,
    132 /*	MAC_EXP_MODE_TEST to test the existence of the named attribute
    133 /*	or MAC_EXP_MODE_USE to use the value of the named attribute,
    134 /*	and the caller context that was given to mac_expand(). A null
    135 /*	result value means that the requested attribute was not defined.
    136 /* .IP don_parse
    137 /*	An optional function that disables the MAC_EXP_FLAG_RECURSE
    138 /*	feature with lookup() results for a specific attribute. Arguments
    139 /*	are: the attribute name, and the caller context that was given
    140 /*	to mac_expand(). Specify null to disable this feature.
    141 /* .IP context
    142 /*	Caller context that is passed on to the attribute lookup and
    143 /*	don_parse routines.
    144 /* .PP
    145 /*	mac_expand_add_relop() registers a function that implements
    146 /*	support for custom relational operators. Custom operator names
    147 /*	such as "==xxx" have two parts: a prefix that is identical to
    148 /*	a built-in operator such as "==", and an application-specified
    149 /*	suffix such as "xxx".
    150 /*
    151 /*	Arguments:
    152 /* .IP tok_list
    153 /*	A null-terminated list of MAC_EXP_OP_TOK_* values that support
    154 /*	the custom operator suffix.
    155 /* .IP suffix
    156 /*	A null-terminated alphanumeric string that specifies the custom
    157 /*	operator suffix.
    158 /* .IP relop_eval
    159 /*	A function that compares two strings according to the
    160 /*	MAC_EXP_OP_TOK_* value specified with the tok_val argument,
    161 /*	and that returns non-zero if the custom operator evaluates to
    162 /*	true, zero otherwise.
    163 /*
    164 /*	mac_exp_op_res_bool provides an array that converts a boolean
    165 /*	value (0 or 1) to the corresponding MAX_EXP_OP_RES_TRUE or
    166 /*	MAX_EXP_OP_RES_FALSE value.
    167 /*
    168 /*	mac_expand_add_named_fn() registers a C function that may be
    169 /*	called as ${name{text}}. The function input is the text after
    170 /*	attribute expansion and expression evaluation. The function
    171 /*	should append its output to the specified buffer, and it
    172 /*	should return either MAC_PARSE_OK or MAC_PARSE_ERROR.
    173 /* DIAGNOSTICS
    174 /*	Fatal errors: out of memory.  Warnings: syntax errors, unreasonable
    175 /*	recursion depth.
    176 /*
    177 /*	The mac_expand() result value is the binary OR of zero or more
    178 /*	of the following:
    179 /* .IP MAC_PARSE_ERROR
    180 /*	A syntax error was found in \fBpattern\fR, or some attribute had
    181 /*	an unreasonable nesting depth.
    182 /* .IP MAC_PARSE_UNDEF
    183 /*	An attribute was expanded but its value was not defined.
    184 /* SEE ALSO
    185 /*	mac_parse(3) locate macro references in string.
    186 /* LICENSE
    187 /* .ad
    188 /* .fi
    189 /*	The Secure Mailer license must be distributed with this software.
    190 /* AUTHOR(S)
    191 /*	Wietse Venema
    192 /*	IBM T.J. Watson Research
    193 /*	P.O. Box 704
    194 /*	Yorktown Heights, NY 10598, USA
    195 /*
    196 /*	Wietse Venema
    197 /*	Google, Inc.
    198 /*	111 8th Avenue
    199 /*	New York, NY 10011, USA
    200 /*
    201 /*	Wietse Venema
    202 /*	porcupine.org
    203 /*--*/
    204 
    205 /* System library. */
    206 
    207 #include <sys_defs.h>
    208 #include <ctype.h>
    209 #include <errno.h>
    210 #include <string.h>
    211 #include <stdlib.h>
    212 
    213 /* Utility library. */
    214 
    215 #include <msg.h>
    216 #include <htable.h>
    217 #include <vstring.h>
    218 #include <mymalloc.h>
    219 #include <stringops.h>
    220 #include <name_code.h>
    221 #include <sane_strtol.h>
    222 #include <mac_parse.h>
    223 #include <mac_expand.h>
    224 
    225  /*
    226   * Simplifies the return of common relational operator results.
    227   */
    228 MAC_EXP_OP_RES mac_exp_op_res_bool[2] = {
    229     MAC_EXP_OP_RES_FALSE,
    230     MAC_EXP_OP_RES_TRUE
    231 };
    232 
    233  /*
    234   * Little helper structure.
    235   */
    236 typedef struct {
    237     VSTRING *result;			/* result buffer */
    238     int     flags;			/* features */
    239     const char *filter;			/* character filter */
    240     MAC_EXP_LOOKUP_FN lookup;		/* lookup routine */
    241     MAC_EXP_DONT_PARSE_FN dont_parse;	/* veto routine */
    242     void   *context;			/* caller context */
    243     int     status;			/* findings */
    244     int     level;			/* nesting level */
    245 } MAC_EXP_CONTEXT;
    246 
    247  /*
    248   * Support for relational expressions.
    249   *
    250   * As of Postfix 2.2, ${attr-name?result} or ${attr-name:result} return the
    251   * result respectively when the parameter value is non-empty, or when the
    252   * parameter value is undefined or empty; support for the ternary ?:
    253   * operator was anticipated, but not implemented for 10 years.
    254   *
    255   * To make ${relational-expr?result} and ${relational-expr:result} work as
    256   * expected without breaking the way that ? and : work, relational
    257   * expressions evaluate to a non-empty or empty value. It does not matter
    258   * what non-empty value we use for TRUE. However we must not use the
    259   * undefined (null pointer) value for FALSE - that would raise the
    260   * MAC_PARSE_UNDEF flag.
    261   *
    262   * The value of a relational expression can be exposed with ${relational-expr},
    263   * i.e. a relational expression that is not followed by ? or : conditional
    264   * expansion.
    265   */
    266 #define MAC_EXP_BVAL_TRUE	"true"
    267 #define MAC_EXP_BVAL_FALSE	""
    268 
    269  /*
    270   * Relational operators. The MAC_EXP_OP_TOK_* are defined in the header
    271   * file.
    272   */
    273 #define MAC_EXP_OP_STR_EQ	"=="
    274 #define MAC_EXP_OP_STR_NE	"!="
    275 #define MAC_EXP_OP_STR_LT	"<"
    276 #define MAC_EXP_OP_STR_LE	"<="
    277 #define MAC_EXP_OP_STR_GE	">="
    278 #define MAC_EXP_OP_STR_GT	">"
    279 #define MAC_EXP_OP_STR_ANY	"\"" MAC_EXP_OP_STR_EQ \
    280 				"\" or \"" MAC_EXP_OP_STR_NE "\"" \
    281 				"\" or \"" MAC_EXP_OP_STR_LT "\"" \
    282 				"\" or \"" MAC_EXP_OP_STR_LE "\"" \
    283 				"\" or \"" MAC_EXP_OP_STR_GE "\"" \
    284 				"\" or \"" MAC_EXP_OP_STR_GT "\""
    285 
    286 static const NAME_CODE mac_exp_op_table[] =
    287 {
    288     MAC_EXP_OP_STR_EQ, MAC_EXP_OP_TOK_EQ,
    289     MAC_EXP_OP_STR_NE, MAC_EXP_OP_TOK_NE,
    290     MAC_EXP_OP_STR_LT, MAC_EXP_OP_TOK_LT,
    291     MAC_EXP_OP_STR_LE, MAC_EXP_OP_TOK_LE,
    292     MAC_EXP_OP_STR_GE, MAC_EXP_OP_TOK_GE,
    293     MAC_EXP_OP_STR_GT, MAC_EXP_OP_TOK_GT,
    294     0, MAC_EXP_OP_TOK_NONE,
    295 };
    296 
    297  /*
    298   * The whitespace separator set.
    299   */
    300 #define MAC_EXP_WHITESPACE	CHARS_SPACE
    301 
    302  /*
    303   * Support for operator extensions.
    304   */
    305 static HTABLE *mac_exp_ext_table;
    306 static VSTRING *mac_exp_ext_key;
    307 
    308  /*
    309   * Support for named functions.
    310   */
    311 static HTABLE *mac_exp_named_fn_table;
    312 struct mac_exp_named_fn_entry {
    313     MAC_EXPAND_NAMED_FN action;
    314 };
    315 
    316  /*
    317   * SLMs.
    318   */
    319 #define STR(x)	vstring_str(x)
    320 
    321 /* atol_or_die - convert or die */
    322 
    323 static long atol_or_die(const char *strval)
    324 {
    325     long    result;
    326     char   *remainder;
    327 
    328     result = sane_strtol(strval, &remainder, 10);
    329     if (*strval == 0 /* can't happen */ || *remainder != 0 || errno == ERANGE)
    330 	msg_fatal("mac_exp_eval: bad conversion: %s", strval);
    331     return (result);
    332 }
    333 
    334 /* mac_exp_eval - evaluate binary expression */
    335 
    336 static MAC_EXP_OP_RES mac_exp_eval(const char *left, int tok_val,
    337 				           const char *rite)
    338 {
    339     static const char myname[] = "mac_exp_eval";
    340     long    delta;
    341 
    342     /*
    343      * Numerical or string comparison.
    344      */
    345     if (alldig(left) && alldig(rite)) {
    346 	delta = atol_or_die(left) - atol_or_die(rite);
    347     } else {
    348 	delta = strcmp(left, rite);
    349     }
    350     switch (tok_val) {
    351     case MAC_EXP_OP_TOK_EQ:
    352 	return (mac_exp_op_res_bool[delta == 0]);
    353     case MAC_EXP_OP_TOK_NE:
    354 	return (mac_exp_op_res_bool[delta != 0]);
    355     case MAC_EXP_OP_TOK_LT:
    356 	return (mac_exp_op_res_bool[delta < 0]);
    357     case MAC_EXP_OP_TOK_LE:
    358 	return (mac_exp_op_res_bool[delta <= 0]);
    359     case MAC_EXP_OP_TOK_GE:
    360 	return (mac_exp_op_res_bool[delta >= 0]);
    361     case MAC_EXP_OP_TOK_GT:
    362 	return (mac_exp_op_res_bool[delta > 0]);
    363     default:
    364 	msg_panic("%s: unknown operator: %d",
    365 		  myname, tok_val);
    366     }
    367 }
    368 
    369 /* mac_exp_parse_error - report parse error, set error flag, return status */
    370 
    371 static int PRINTFLIKE(2, 3) mac_exp_parse_error(MAC_EXP_CONTEXT *mc,
    372 						        const char *fmt,...)
    373 {
    374     va_list ap;
    375 
    376     va_start(ap, fmt);
    377     vmsg_warn(fmt, ap);
    378     va_end(ap);
    379     return (mc->status |= MAC_PARSE_ERROR);
    380 };
    381 
    382 /* MAC_EXP_ERR_RETURN - report parse error, set error flag, return status */
    383 
    384 #define MAC_EXP_ERR_RETURN(mc, fmt, ...) do { \
    385 	return (mac_exp_parse_error(mc, fmt, __VA_ARGS__)); \
    386     } while (0)
    387 
    388  /*
    389   * Postfix 3.0 introduces support for {text} operands. Only with these do we
    390   * support the ternary ?: operator and relational operators.
    391   *
    392   * We cannot support operators in random text, because that would break Postfix
    393   * 2.11 compatibility. For example, with the expression "${name?value}", the
    394   * value is random text that may contain ':', '?', '{' and '}' characters.
    395   * In particular, with Postfix 2.2 .. 2.11, "${name??foo:{b}ar}" evaluates
    396   * to "?foo:{b}ar" or empty. There are explicit tests in this directory and
    397   * the postconf directory to ensure that Postfix 2.11 compatibility is
    398   * maintained.
    399   *
    400   * Ideally, future Postfix configurations enclose random text operands inside
    401   * {} braces. These allow whitespace around operands, which improves
    402   * readability.
    403   */
    404 
    405 /* MAC_EXP_FIND_LEFT_CURLY - skip over whitespace to '{', advance read ptr */
    406 
    407 #define MAC_EXP_FIND_LEFT_CURLY(len, cp) \
    408 	((cp[len = strspn(cp, MAC_EXP_WHITESPACE)] == '{') ? \
    409 	 (cp += len) : 0)
    410 
    411 /* mac_exp_extract_curly_payload - balance {}, skip whitespace, return payload */
    412 
    413 static char *mac_exp_extract_curly_payload(MAC_EXP_CONTEXT *mc, char **bp,
    414 					           int strip_space)
    415 {
    416     char   *payload;
    417     char   *cp;
    418     int     level;
    419     int     ch;
    420 
    421 #define DO_STRIP_SPACE	1
    422 #define NO_STRIP_SPACE	0
    423 
    424     /*
    425      * Extract the payload and balance the {}. The caller is expected to skip
    426      * leading whitespace before the {. See MAC_EXP_FIND_LEFT_CURLY().
    427      * TODO(wietse) this code pre-dates extpar() and mainly differs in how an
    428      * error is reported.
    429      */
    430     payload = *bp + 1;
    431     if (strip_space)
    432 	payload += strspn(payload, MAC_EXP_WHITESPACE);
    433     for (level = 1, cp = payload; /* see below */ ; cp++) {
    434 	if ((ch = *cp) == 0) {
    435 	    mac_exp_parse_error(mc, "unbalanced {} in attribute expression: "
    436 				"\"%s\"",
    437 				*bp);
    438 	    return (0);
    439 	} else if (ch == '{') {
    440 	    level++;
    441 	} else if (ch == '}') {
    442 	    if (--level <= 0)
    443 		break;
    444 	}
    445     }
    446     if (strip_space)
    447 	trimblanks(payload, cp - payload)[0] = 0;
    448     *cp++ = 0;
    449 
    450     /*
    451      * Skip trailing whitespace after }.
    452      */
    453     *bp = cp + strspn(cp, MAC_EXP_WHITESPACE);
    454     return (payload);
    455 }
    456 
    457 /* mac_exp_parse_relational - parse relational expression, advance read ptr */
    458 
    459 static int mac_exp_parse_relational(MAC_EXP_CONTEXT *mc, const char **lookup,
    460 				            char **bp)
    461 {
    462     char   *cp = *bp;
    463     VSTRING *left_op_buf;
    464     VSTRING *rite_op_buf;
    465     const char *left_op_strval;
    466     const char *rite_op_strval;
    467     char   *op_pos;
    468     char   *op_strval;
    469     size_t  op_len;
    470     int     op_tokval;
    471     int     op_result;
    472     size_t  tmp_len;
    473     char   *type_pos;
    474     size_t  type_len;
    475     MAC_EXPAND_RELOP_FN relop_eval;
    476 
    477     /*
    478      * Left operand. The caller is expected to skip leading whitespace before
    479      * the {. See MAC_EXP_FIND_LEFT_CURLY().
    480      */
    481     if ((left_op_strval = mac_exp_extract_curly_payload(mc, &cp,
    482 						      NO_STRIP_SPACE)) == 0)
    483 	return (mc->status);
    484 
    485     /*
    486      * Operator. Todo: regexp operator.
    487      */
    488     op_pos = cp;
    489     op_len = strspn(cp, "<>!=?+-*/~&|%");	/* for better diagnostics. */
    490     op_strval = mystrndup(cp, op_len);
    491     op_tokval = name_code(mac_exp_op_table, NAME_CODE_FLAG_NONE, op_strval);
    492     myfree(op_strval);
    493     if (op_tokval == MAC_EXP_OP_TOK_NONE)
    494 	MAC_EXP_ERR_RETURN(mc, "%s expected at: \"...%s}>>>%.20s\"",
    495 			   MAC_EXP_OP_STR_ANY, left_op_strval, cp);
    496     cp += op_len;
    497 
    498     /*
    499      * Custom operator suffix.
    500      */
    501     if (mac_exp_ext_table && ISALNUM(*cp)) {
    502 	type_pos = cp;
    503 	for (type_len = 1; ISALNUM(cp[type_len]); type_len++)
    504 	     /* void */ ;
    505 	cp += type_len;
    506 	vstring_sprintf(mac_exp_ext_key, "%.*s",
    507 			(int) (op_len + type_len), op_pos);
    508 	if ((relop_eval = (MAC_EXPAND_RELOP_FN) htable_find(mac_exp_ext_table,
    509 						STR(mac_exp_ext_key))) == 0)
    510 	    MAC_EXP_ERR_RETURN(mc, "bad operator suffix at: \"...%.*s>>>%.*s\"",
    511 			    (int) op_len, op_pos, (int) type_len, type_pos);
    512     } else {
    513 	relop_eval = mac_exp_eval;
    514     }
    515 
    516     /*
    517      * Right operand. Todo: syntax may depend on operator.
    518      */
    519     if (MAC_EXP_FIND_LEFT_CURLY(tmp_len, cp) == 0)
    520 	MAC_EXP_ERR_RETURN(mc, "\"{expression}\" expected at: "
    521 			   "\"...{%s} %.*s>>>%.20s\"",
    522 			   left_op_strval, (int) op_len, op_pos, cp);
    523     if ((rite_op_strval = mac_exp_extract_curly_payload(mc, &cp,
    524 						      NO_STRIP_SPACE)) == 0)
    525 	return (mc->status);
    526 
    527     /*
    528      * Evaluate the relational expression. Todo: regexp support.
    529      */
    530     mc->status |=
    531 	mac_expand(left_op_buf = vstring_alloc(100), left_op_strval,
    532 		   mc->flags, mc->filter, mc->lookup, mc->context);
    533     mc->status |=
    534 	mac_expand(rite_op_buf = vstring_alloc(100), rite_op_strval,
    535 		   mc->flags, mc->filter, mc->lookup, mc->context);
    536     if ((mc->flags & MAC_EXP_FLAG_SCAN) == 0
    537 	&& (op_result = relop_eval(vstring_str(left_op_buf), op_tokval,
    538 			 vstring_str(rite_op_buf))) == MAC_EXP_OP_RES_ERROR)
    539 	mc->status |= MAC_PARSE_ERROR;
    540     vstring_free(left_op_buf);
    541     vstring_free(rite_op_buf);
    542     if (mc->status & MAC_PARSE_ERROR)
    543 	return (mc->status);
    544 
    545     /*
    546      * Here, we fake up a non-empty or empty parameter value lookup result,
    547      * for compatibility with the historical code that looks named parameter
    548      * values.
    549      */
    550     if (mc->flags & MAC_EXP_FLAG_SCAN) {
    551 	*lookup = 0;
    552     } else {
    553 	switch (op_result) {
    554 	case MAC_EXP_OP_RES_TRUE:
    555 	    *lookup = MAC_EXP_BVAL_TRUE;
    556 	    break;
    557 	case MAC_EXP_OP_RES_FALSE:
    558 	    *lookup = MAC_EXP_BVAL_FALSE;
    559 	    break;
    560 	default:
    561 	    msg_panic("mac_expand: unexpected operator result: %d", op_result);
    562 	}
    563     }
    564     *bp = cp;
    565     return (0);
    566 }
    567 
    568 /* mac_expand_add_relop - register operator extensions */
    569 
    570 void    mac_expand_add_relop(int *tok_list, const char *suffix,
    571 			             MAC_EXPAND_RELOP_FN relop_eval)
    572 {
    573     const char myname[] = "mac_expand_add_relop";
    574     const char *tok_name;
    575     int    *tp;
    576 
    577     /*
    578      * Sanity checks.
    579      */
    580     if (!allalnum(suffix))
    581 	msg_panic("%s: bad operator suffix: %s", myname, suffix);
    582 
    583     /*
    584      * One-time initialization.
    585      */
    586     if (mac_exp_ext_table == 0) {
    587 	mac_exp_ext_table = htable_create(10);
    588 	mac_exp_ext_key = vstring_alloc(10);
    589     }
    590     for (tp = tok_list; *tp; tp++) {
    591 	if ((tok_name = str_name_code(mac_exp_op_table, *tp)) == 0)
    592 	    msg_panic("%s: unknown token code: %d", myname, *tp);
    593 	vstring_sprintf(mac_exp_ext_key, "%s%s", tok_name, suffix);
    594 	if (htable_locate(mac_exp_ext_table, STR(mac_exp_ext_key)) != 0)
    595 	    msg_panic("%s: duplicate key: %s", myname, STR(mac_exp_ext_key));
    596 	(void) htable_enter(mac_exp_ext_table,
    597 			    STR(mac_exp_ext_key), (void *) relop_eval);
    598     }
    599 }
    600 
    601 /* mac_expand_add_named_fn - register named-function callback */
    602 
    603 void    mac_expand_add_named_fn(const char *name, MAC_EXPAND_NAMED_FN action)
    604 {
    605     struct mac_exp_named_fn_entry *xp;
    606 
    607     /*
    608      * Sanity checks.
    609      */
    610     if (!allalnumus(name))
    611 	msg_panic("%s: bad function name: \"%s\"", __func__, name);
    612 
    613     /*
    614      * One-time initialization.
    615      */
    616     if (mac_exp_named_fn_table == 0)
    617 	mac_exp_named_fn_table = htable_create(10);
    618 
    619     /*
    620      * The C language spec allows sizeof(void *) < sizeof(function pointer).
    621      */
    622     if (htable_locate(mac_exp_named_fn_table, name) != 0)
    623 	msg_panic("%s: duplicate key: %s", __func__, name);
    624     xp = (struct mac_exp_named_fn_entry *) mymalloc(sizeof(*xp));
    625     xp->action = action;
    626     (void) htable_enter(mac_exp_named_fn_table, name, (void *) xp);
    627 }
    628 
    629 /* mac_exp_parse_function - interpolate result from caller-defined function */
    630 
    631 static int mac_exp_parse_function(char *name_start, char *name_end,
    632 				          char *cp, MAC_EXP_CONTEXT *mc)
    633 {
    634     char   *fn_arg;
    635     struct mac_exp_named_fn_entry *xp;
    636     VSTRING *buf = 0;
    637 
    638     /* cp is positioned at the '{', zero or more spaces after name_end. */
    639     if ((fn_arg = mac_exp_extract_curly_payload(mc, &cp, DO_STRIP_SPACE)) == 0)
    640 	return (mc->status);
    641     if (*cp != 0)				/* garbage */
    642 	MAC_EXP_ERR_RETURN(mc, "unexpected input at: "
    643 			   "\"...%s}>>>%.20s\"", fn_arg, cp);
    644     *name_end = 0;
    645     /* Look up the function and evaluate. */
    646     xp = (struct mac_exp_named_fn_entry *)
    647 	htable_find(mac_exp_named_fn_table, name_start);
    648     if (xp == 0)
    649 	MAC_EXP_ERR_RETURN(mc, "unknown function \"%s\" at"
    650 			"\"...>>>%s{%s}\"", name_start, name_start, fn_arg);
    651     if ((mc->flags & MAC_EXP_FLAG_SCAN) == 0)
    652 	buf = vstring_alloc(100);
    653     mc->status |=
    654 	mac_expand(buf, fn_arg, mc->flags, mc->filter, mc->lookup,
    655 		   mc->context);
    656     if ((mc->flags & MAC_EXP_FLAG_SCAN) == 0) {
    657 	if ((mc->status & MAC_PARSE_ERROR) == 0
    658 	    && xp->action(mc->result, vstring_str(buf)) == MAC_PARSE_ERROR)
    659 	    mc->status |= MAC_PARSE_ERROR;
    660 	vstring_free(buf);
    661     }
    662     return (mc->status);
    663 }
    664 
    665 /* mac_expand_callback - callback for mac_parse */
    666 
    667 static int mac_expand_callback(int type, VSTRING *buf, void *ptr)
    668 {
    669     static const char myname[] = "mac_expand_callback";
    670     MAC_EXP_CONTEXT *mc = (MAC_EXP_CONTEXT *) ptr;
    671     int     lookup_mode;
    672     const char *lookup;
    673     char   *cp;
    674     int     ch;
    675     ssize_t res_len;
    676     ssize_t tmp_len;
    677     const char *res_iftrue;
    678     const char *res_iffalse;
    679     int     dont_parse = false;
    680 
    681     /*
    682      * Sanity check.
    683      */
    684     if (mc->level++ > 100)
    685 	mac_exp_parse_error(mc, "unreasonable macro call nesting: \"%s\"",
    686 			    vstring_str(buf));
    687     if (mc->status & MAC_PARSE_ERROR)
    688 	return (mc->status);
    689 
    690     /*
    691      * Named parameter, function call, or relational expression. In case of a
    692      * syntax error, return without doing damage, and issue a warning
    693      * instead.
    694      */
    695     if (type == MAC_PARSE_EXPR) {
    696 
    697 	cp = vstring_str(buf);
    698 
    699 	/*
    700 	 * Relational expression. If recursion is disabled, perform only one
    701 	 * level of $name expansion.
    702 	 */
    703 	if (MAC_EXP_FIND_LEFT_CURLY(tmp_len, cp)) {
    704 	    if (mac_exp_parse_relational(mc, &lookup, &cp) != 0)
    705 		return (mc->status);
    706 
    707 	    /*
    708 	     * Look for the ? or : operator.
    709 	     */
    710 	    if ((ch = *cp) != 0) {
    711 		if (ch != '?' && ch != ':')
    712 		    MAC_EXP_ERR_RETURN(mc, "\"?\" or \":\" expected at: "
    713 				       "\"...}>>>%.20s\"", cp);
    714 		cp++;
    715 	    }
    716 	}
    717 
    718 	/*
    719 	 * Named parameter or function call.
    720 	 */
    721 	else {
    722 	    char   *start;
    723 
    724 	    /*
    725 	     * Collect the name of the parameter or function. Look for the ?
    726 	     * or : operator, or the '{' to indicate a function call. In case
    727 	     * of a syntax error, return without doing damage, and issue a
    728 	     * warning instead.
    729 	     */
    730 	    start = (cp += strspn(cp, MAC_EXP_WHITESPACE));
    731 	    for ( /* void */ ; /* void */ ; cp++) {
    732 		if ((ch = cp[tmp_len = strspn(cp, MAC_EXP_WHITESPACE)]) == 0) {
    733 		    *cp = 0;
    734 		    lookup_mode = MAC_EXP_MODE_USE;
    735 		    break;
    736 		}
    737 		if (ch == '?' || ch == ':') {
    738 		    *cp++ = 0;
    739 		    cp += tmp_len;
    740 		    lookup_mode = MAC_EXP_MODE_TEST;
    741 		    break;
    742 		}
    743 		if (ch == '{')
    744 		    return (mac_exp_parse_function(start, cp, cp + tmp_len, mc));
    745 		ch = *cp;
    746 		if (!ISALNUM(ch) && ch != '_') {
    747 		    MAC_EXP_ERR_RETURN(mc, "attribute name syntax error at: "
    748 				       "\"...%.*s>>>%.20s\"",
    749 				       (int) (cp - vstring_str(buf)),
    750 				       vstring_str(buf), cp);
    751 		}
    752 	    }
    753 
    754 	    /*
    755 	     * Look up the named parameter. Todo: allow the lookup function
    756 	     * to specify if the result is safe for $name expansion.
    757 	     */
    758 	    if (mc->dont_parse)
    759 		dont_parse = mc->dont_parse(start, mc->context);
    760 	    lookup = mc->lookup(start, lookup_mode, mc->context);
    761 	}
    762 
    763 	/*
    764 	 * Return the requested result. After parsing the result operand
    765 	 * following ?, we fall through to parse the result operand following
    766 	 * :. This is necessary with the ternary ?: operator: first, with
    767 	 * MAC_EXP_FLAG_SCAN to parse both result operands with mac_parse(),
    768 	 * and second, to find garbage after any result operand. Without
    769 	 * MAC_EXP_FLAG_SCAN the content of only one of the ?: result
    770 	 * operands will be parsed with mac_parse(); syntax errors in the
    771 	 * other operand will be missed.
    772 	 */
    773 	switch (ch) {
    774 	case '?':
    775 	    if (MAC_EXP_FIND_LEFT_CURLY(tmp_len, cp)) {
    776 		if ((res_iftrue = mac_exp_extract_curly_payload(mc, &cp,
    777 						      NO_STRIP_SPACE)) == 0)
    778 		    return (mc->status);
    779 	    } else {
    780 		res_iftrue = cp;
    781 		cp = "";			/* no left-over text */
    782 	    }
    783 	    if ((lookup != 0 && *lookup != 0) || (mc->flags & MAC_EXP_FLAG_SCAN))
    784 		mc->status |= mac_parse(res_iftrue, mac_expand_callback,
    785 					(void *) mc);
    786 	    if (*cp == 0)			/* end of input, OK */
    787 		break;
    788 	    if (*cp != ':')			/* garbage */
    789 		MAC_EXP_ERR_RETURN(mc, "\":\" expected at: "
    790 				   "\"...%s}>>>%.20s\"", res_iftrue, cp);
    791 	    cp += 1;
    792 	    /* FALLTHROUGH: do not remove, see comment above. */
    793 	case ':':
    794 	    if (MAC_EXP_FIND_LEFT_CURLY(tmp_len, cp)) {
    795 		if ((res_iffalse = mac_exp_extract_curly_payload(mc, &cp,
    796 						      NO_STRIP_SPACE)) == 0)
    797 		    return (mc->status);
    798 	    } else {
    799 		res_iffalse = cp;
    800 		cp = "";			/* no left-over text */
    801 	    }
    802 	    if (lookup == 0 || *lookup == 0 || (mc->flags & MAC_EXP_FLAG_SCAN))
    803 		mc->status |= mac_parse(res_iffalse, mac_expand_callback,
    804 					(void *) mc);
    805 	    if (*cp != 0)			/* garbage */
    806 		MAC_EXP_ERR_RETURN(mc, "unexpected input at: "
    807 				   "\"...%s}>>>%.20s\"", res_iffalse, cp);
    808 	    break;
    809 	case 0:
    810 	    if (lookup == 0) {
    811 		mc->status |= MAC_PARSE_UNDEF;
    812 	    } else if (*lookup == 0 || (mc->flags & MAC_EXP_FLAG_SCAN)) {
    813 		 /* void */ ;
    814 	    } else if ((mc->flags & MAC_EXP_FLAG_RECURSE) && !dont_parse) {
    815 		vstring_strcpy(buf, lookup);
    816 		mc->status |= mac_parse(vstring_str(buf), mac_expand_callback,
    817 					(void *) mc);
    818 	    } else {
    819 		res_len = VSTRING_LEN(mc->result);
    820 		vstring_strcat(mc->result, lookup);
    821 		if (mc->flags & MAC_EXP_FLAG_PRINTABLE) {
    822 		    printable(vstring_str(mc->result) + res_len, '_');
    823 		} else if (mc->filter) {
    824 		    cp = vstring_str(mc->result) + res_len;
    825 		    while (*(cp += strspn(cp, mc->filter)))
    826 			*cp++ = '_';
    827 		}
    828 	    }
    829 	    break;
    830 	default:
    831 	    msg_panic("%s: unknown operator code %d", myname, ch);
    832 	}
    833     }
    834 
    835     /*
    836      * Literal text.
    837      */
    838     else if ((mc->flags & MAC_EXP_FLAG_SCAN) == 0) {
    839 	vstring_strcat(mc->result, vstring_str(buf));
    840     }
    841     mc->level--;
    842 
    843     return (mc->status);
    844 }
    845 
    846  /*
    847   * ABI compatibility wrapper.
    848   */
    849 #undef mac_expand
    850 int     mac_expand(VSTRING *, const char *, int, const char *,
    851 		           MAC_EXP_LOOKUP_FN, void *);
    852 
    853 /* mac_expand - expand $name instances */
    854 
    855 int     mac_expand(VSTRING *result, const char *pattern, int flags,
    856 		           const char *filter,
    857 		           MAC_EXP_LOOKUP_FN lookup, void *context)
    858 {
    859     return (mac_expand7(result, pattern, flags, filter, lookup,
    860 			(MAC_EXP_DONT_PARSE_FN) 0, context));
    861 }
    862 
    863 int     mac_expand7(VSTRING *result, const char *pattern, int flags,
    864 		            const char *filter,
    865 		            MAC_EXP_LOOKUP_FN lookup,
    866 		            MAC_EXP_DONT_PARSE_FN dont_parse, void *context)
    867 {
    868 
    869     MAC_EXP_CONTEXT mc;
    870     int     status;
    871 
    872     /*
    873      * Bundle up the request and do the substitutions.
    874      */
    875     mc.result = result;
    876     mc.flags = flags;
    877     mc.filter = filter;
    878     mc.lookup = lookup;
    879     mc.dont_parse = dont_parse;
    880     mc.context = context;
    881     mc.status = 0;
    882     mc.level = 0;
    883     if ((flags & (MAC_EXP_FLAG_APPEND | MAC_EXP_FLAG_SCAN)) == 0)
    884 	VSTRING_RESET(result);
    885     status = mac_parse(pattern, mac_expand_callback, (void *) &mc);
    886     if ((flags & MAC_EXP_FLAG_SCAN) == 0)
    887 	VSTRING_TERMINATE(result);
    888 
    889     return (status);
    890 }
    891 
    892 #ifdef TEST
    893 
    894  /*
    895   * This code certainly deserves a stand-alone test program.
    896   */
    897 #include <stringops.h>
    898 #include <htable.h>
    899 #include <vstream.h>
    900 #include <vstring_vstream.h>
    901 
    902 static const char *lookup(const char *name, int unused_mode, void *context)
    903 {
    904     HTABLE *table = (HTABLE *) context;
    905 
    906     return (htable_find(table, name));
    907 }
    908 
    909 static MAC_EXP_OP_RES length_relop_eval(const char *left, int relop,
    910 					        const char *rite)
    911 {
    912     const char myname[] = "length_relop_eval";
    913     ssize_t delta = strlen(left) - strlen(rite);
    914 
    915     switch (relop) {
    916     case MAC_EXP_OP_TOK_EQ:
    917 	return (mac_exp_op_res_bool[delta == 0]);
    918     case MAC_EXP_OP_TOK_NE:
    919 	return (mac_exp_op_res_bool[delta != 0]);
    920     case MAC_EXP_OP_TOK_LT:
    921 	return (mac_exp_op_res_bool[delta < 0]);
    922     case MAC_EXP_OP_TOK_LE:
    923 	return (mac_exp_op_res_bool[delta <= 0]);
    924     case MAC_EXP_OP_TOK_GE:
    925 	return (mac_exp_op_res_bool[delta >= 0]);
    926     case MAC_EXP_OP_TOK_GT:
    927 	return (mac_exp_op_res_bool[delta > 0]);
    928     default:
    929 	msg_panic("%s: unknown operator: %d",
    930 		  myname, relop);
    931     }
    932 }
    933 
    934 static int named_fn_eval(VSTRING *out, const char *in)
    935 {
    936     vstring_sprintf_append(out, "-=oO%sOo=-", in);
    937     return (0);
    938 }
    939 
    940 int     main(int unused_argc, char **argv)
    941 {
    942     VSTRING *buf = vstring_alloc(100);
    943     VSTRING *result = vstring_alloc(100);
    944     char   *cp;
    945     char   *name;
    946     char   *value;
    947     HTABLE *table;
    948     int     stat;
    949     int     length_relops[] = {
    950 	MAC_EXP_OP_TOK_EQ, MAC_EXP_OP_TOK_NE,
    951 	MAC_EXP_OP_TOK_GT, MAC_EXP_OP_TOK_GE,
    952 	MAC_EXP_OP_TOK_LT, MAC_EXP_OP_TOK_LE,
    953 	0,
    954     };
    955 
    956     /*
    957      * Add relops that compare string lengths instead of content.
    958      */
    959     mac_expand_add_relop(length_relops, "length", length_relop_eval);
    960 
    961     /*
    962      * Add a silly named function that decorates its argument.
    963      */
    964     mac_expand_add_named_fn("test_named_fn", named_fn_eval);
    965 
    966     /*
    967      * Loop over the inputs.
    968      */
    969     while (!vstream_feof(VSTREAM_IN)) {
    970 
    971 	table = htable_create(0);
    972 
    973 	/*
    974 	 * Read a block of definitions, terminated with an empty line.
    975 	 */
    976 	while (vstring_get_nonl(buf, VSTREAM_IN) != VSTREAM_EOF) {
    977 	    vstream_printf("<< %s\n", vstring_str(buf));
    978 	    vstream_fflush(VSTREAM_OUT);
    979 	    if (VSTRING_LEN(buf) == 0)
    980 		break;
    981 	    cp = vstring_str(buf);
    982 	    name = mystrtok(&cp, CHARS_SPACE "=");
    983 	    value = mystrtok(&cp, CHARS_SPACE "=");
    984 	    htable_enter(table, name, value ? mystrdup(value) : 0);
    985 	}
    986 
    987 	/*
    988 	 * Read a block of patterns, terminated with an empty line or EOF.
    989 	 */
    990 	while (vstring_get_nonl(buf, VSTREAM_IN) != VSTREAM_EOF) {
    991 	    vstream_printf("<< %s\n", vstring_str(buf));
    992 	    vstream_fflush(VSTREAM_OUT);
    993 	    if (VSTRING_LEN(buf) == 0)
    994 		break;
    995 	    cp = vstring_str(buf);
    996 	    VSTRING_RESET(result);
    997 	    stat = mac_expand(result, vstring_str(buf), MAC_EXP_FLAG_NONE,
    998 			      (char *) 0, lookup, (void *) table);
    999 	    vstream_printf("stat=%d result=%s\n", stat, vstring_str(result));
   1000 	    vstream_fflush(VSTREAM_OUT);
   1001 	}
   1002 	htable_free(table, myfree);
   1003 	vstream_printf("\n");
   1004     }
   1005 
   1006     /*
   1007      * Clean up.
   1008      */
   1009     vstring_free(buf);
   1010     vstring_free(result);
   1011     exit(0);
   1012 }
   1013 
   1014 #endif
   1015