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