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