diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-27 12:06:34 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-27 12:06:34 +0000 |
commit | 5e61585d76ae77fd5e9e96ebabb57afa4d74880d (patch) | |
tree | 2b467823aaeebc7ef8bc9e3cabe8074eaef1666d /src/util/mac_expand.c | |
parent | Initial commit. (diff) | |
download | postfix-5e61585d76ae77fd5e9e96ebabb57afa4d74880d.tar.xz postfix-5e61585d76ae77fd5e9e96ebabb57afa4d74880d.zip |
Adding upstream version 3.5.24.upstream/3.5.24
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/util/mac_expand.c')
-rw-r--r-- | src/util/mac_expand.c | 693 |
1 files changed, 693 insertions, 0 deletions
diff --git a/src/util/mac_expand.c b/src/util/mac_expand.c new file mode 100644 index 0000000..1679e5e --- /dev/null +++ b/src/util/mac_expand.c @@ -0,0 +1,693 @@ +/*++ +/* NAME +/* mac_expand 3 +/* SUMMARY +/* attribute expansion +/* SYNOPSIS +/* #include <mac_expand.h> +/* +/* int mac_expand(result, pattern, flags, filter, lookup, context) +/* VSTRING *result; +/* const char *pattern; +/* int flags; +/* const char *filter; +/* const char *lookup(const char *key, int mode, void *context) +/* void *context; +/* DESCRIPTION +/* This module implements parameter-less named attribute +/* expansions, both conditional and unconditional. As of Postfix +/* 3.0 this code supports relational expression evaluation. +/* +/* In this text, an attribute is considered "undefined" when its value +/* is a null pointer. Otherwise, the attribute is considered "defined" +/* and is expected to have as value a null-terminated string. +/* +/* In the text below, the legacy form $(...) is equivalent to +/* ${...}. The legacy form $(...) may eventually disappear +/* from documentation. In the text below, the name in $name +/* and ${name...} must contain only characters from the set +/* [a-zA-Z0-9_]. +/* +/* The following substitutions are supported: +/* .IP "$name, ${name}" +/* Unconditional attribute-based substition. The result is the +/* named attribute value (empty if the attribute is not defined) +/* after optional further named attribute substitution. +/* .IP "${name?text}, ${name?{text}}" +/* Conditional attribute-based substition. If the named attribute +/* value is non-empty, the result is the given text, after +/* named attribute expansion and relational expression evaluation. +/* Otherwise, the result is empty. Whitespace before or after +/* {text} is ignored. +/* .IP "${name:text}, ${name:{text}}" +/* Conditional attribute-based substition. If the attribute +/* value is empty or undefined, the expansion is the given +/* text, after named attribute expansion and relational expression +/* evaluation. Otherwise, the result is empty. Whitespace +/* before or after {text} is ignored. +/* .IP "${name?{text1}:{text2}}, ${name?{text1}:text2}" +/* Conditional attribute-based substition. If the named attribute +/* value is non-empty, the result is text1. Otherwise, the +/* result is text2. In both cases the result is subject to +/* named attribute expansion and relational expression evaluation. +/* Whitespace before or after {text1} or {text2} is ignored. +/* .IP "${{text1} == ${text2} ? {text3} : {text4}}" +/* Relational expression-based substition. First, the content +/* of {text1} and ${text2} is subjected to named attribute and +/* relational expression-based substitution. Next, the relational +/* expression is evaluated. If it evaluates to "true", the +/* result is the content of {text3}, otherwise it is the content +/* of {text4}, after named attribute and relational expression-based +/* substitution. In addition to ==, this supports !=, <, <=, +/* >=, and >. Comparisons are numerical when both operands are +/* all digits, otherwise the comparisons are lexicographical. +/* +/* Arguments: +/* .IP result +/* Storage for the result of expansion. By default, the result +/* is truncated upon entry. +/* .IP pattern +/* The string to be expanded. +/* .IP flags +/* Bit-wise OR of zero or more of the following: +/* .RS +/* .IP MAC_EXP_FLAG_RECURSE +/* Expand attributes in lookup results. This should never be +/* done with data whose origin is untrusted. +/* .IP MAC_EXP_FLAG_APPEND +/* Append text to the result buffer without truncating it. +/* .IP MAC_EXP_FLAG_SCAN +/* Scan the input for named attributes, including named +/* attributes in all conditional result values. Do not expand +/* named attributes, and do not truncate or write to the result +/* argument. +/* .IP MAC_EXP_FLAG_PRINTABLE +/* Use the printable() function instead of \fIfilter\fR. +/* .PP +/* The constant MAC_EXP_FLAG_NONE specifies a manifest null value. +/* .RE +/* .IP filter +/* A null pointer, or a null-terminated array of characters that +/* are allowed to appear in an expansion. Illegal characters are +/* replaced by underscores. +/* .IP lookup +/* The attribute lookup routine. Arguments are: the attribute name, +/* MAC_EXP_MODE_TEST to test the existence of the named attribute +/* or MAC_EXP_MODE_USE to use the value of the named attribute, +/* and the caller context that was given to mac_expand(). A null +/* result value means that the requested attribute was not defined. +/* .IP context +/* Caller context that is passed on to the attribute lookup routine. +/* DIAGNOSTICS +/* Fatal errors: out of memory. Warnings: syntax errors, unreasonable +/* recursion depth. +/* +/* The result value is the binary OR of zero or more of the following: +/* .IP MAC_PARSE_ERROR +/* A syntax error was found in \fBpattern\fR, or some attribute had +/* an unreasonable nesting depth. +/* .IP MAC_PARSE_UNDEF +/* An attribute was expanded but its value was not defined. +/* SEE ALSO +/* mac_parse(3) locate macro references in string. +/* LICENSE +/* .ad +/* .fi +/* The Secure Mailer license must be distributed with this software. +/* AUTHOR(S) +/* Wietse Venema +/* IBM T.J. Watson Research +/* P.O. Box 704 +/* Yorktown Heights, NY 10598, USA +/* +/* Wietse Venema +/* Google, Inc. +/* 111 8th Avenue +/* New York, NY 10011, USA +/*--*/ + +/* System library. */ + +#include <sys_defs.h> +#include <ctype.h> +#include <errno.h> +#include <string.h> +#include <stdlib.h> + +/* Utility library. */ + +#include <msg.h> +#include <vstring.h> +#include <mymalloc.h> +#include <stringops.h> +#include <name_code.h> +#include <mac_parse.h> +#include <mac_expand.h> + + /* + * Little helper structure. + */ +typedef struct { + VSTRING *result; /* result buffer */ + int flags; /* features */ + const char *filter; /* character filter */ + MAC_EXP_LOOKUP_FN lookup; /* lookup routine */ + void *context; /* caller context */ + int status; /* findings */ + int level; /* nesting level */ +} MAC_EXP_CONTEXT; + + /* + * Support for relational expressions. + * + * As of Postfix 2.2, ${attr-name?result} or ${attr-name:result} return the + * result respectively when the parameter value is non-empty, or when the + * parameter value is undefined or empty; support for the ternary ?: + * operator was anticipated, but not implemented for 10 years. + * + * To make ${relational-expr?result} and ${relational-expr:result} work as + * expected without breaking the way that ? and : work, relational + * expressions evaluate to a non-empty or empty value. It does not matter + * what non-empty value we use for TRUE. However we must not use the + * undefined (null pointer) value for FALSE - that would raise the + * MAC_PARSE_UNDEF flag. + * + * The value of a relational expression can be exposed with ${relational-expr}, + * i.e. a relational expression that is not followed by ? or : conditional + * expansion. + */ +#define MAC_EXP_BVAL_TRUE "true" +#define MAC_EXP_BVAL_FALSE "" + + /* + * Relational operators. + */ +#define MAC_EXP_OP_STR_EQ "==" +#define MAC_EXP_OP_STR_NE "!=" +#define MAC_EXP_OP_STR_LT "<" +#define MAC_EXP_OP_STR_LE "<=" +#define MAC_EXP_OP_STR_GE ">=" +#define MAC_EXP_OP_STR_GT ">" +#define MAC_EXP_OP_STR_ANY "\"" MAC_EXP_OP_STR_EQ \ + "\" or \"" MAC_EXP_OP_STR_NE "\"" \ + "\" or \"" MAC_EXP_OP_STR_LT "\"" \ + "\" or \"" MAC_EXP_OP_STR_LE "\"" \ + "\" or \"" MAC_EXP_OP_STR_GE "\"" \ + "\" or \"" MAC_EXP_OP_STR_GT "\"" + +#define MAC_EXP_OP_TOK_NONE 0 +#define MAC_EXP_OP_TOK_EQ 1 +#define MAC_EXP_OP_TOK_NE 2 +#define MAC_EXP_OP_TOK_LT 3 +#define MAC_EXP_OP_TOK_LE 4 +#define MAC_EXP_OP_TOK_GE 5 +#define MAC_EXP_OP_TOK_GT 6 + +static const NAME_CODE mac_exp_op_table[] = +{ + MAC_EXP_OP_STR_EQ, MAC_EXP_OP_TOK_EQ, + MAC_EXP_OP_STR_NE, MAC_EXP_OP_TOK_NE, + MAC_EXP_OP_STR_LT, MAC_EXP_OP_TOK_LT, + MAC_EXP_OP_STR_LE, MAC_EXP_OP_TOK_LE, + MAC_EXP_OP_STR_GE, MAC_EXP_OP_TOK_GE, + MAC_EXP_OP_STR_GT, MAC_EXP_OP_TOK_GT, + 0, MAC_EXP_OP_TOK_NONE, +}; + + /* + * The whitespace separator set. + */ +#define MAC_EXP_WHITESPACE CHARS_SPACE + +/* atol_or_die - convert or die */ + +static long atol_or_die(const char *strval) +{ + long result; + char *remainder; + + errno = 0; + result = strtol(strval, &remainder, 10); + if (*strval == 0 /* can't happen */ || *remainder != 0 || errno == ERANGE) + msg_fatal("mac_exp_eval: bad conversion: %s", strval); + return (result); +} + +/* mac_exp_eval - evaluate binary expression */ + +static int mac_exp_eval(const char *left, int tok_val, + const char *rite) +{ + static const char myname[] = "mac_exp_eval"; + long delta; + + /* + * Numerical or string comparison. + */ + if (alldig(left) && alldig(rite)) { + delta = atol_or_die(left) - atol_or_die(rite); + } else { + delta = strcmp(left, rite); + } + switch (tok_val) { + case MAC_EXP_OP_TOK_EQ: + return (delta == 0); + case MAC_EXP_OP_TOK_NE: + return (delta != 0); + case MAC_EXP_OP_TOK_LT: + return (delta < 0); + case MAC_EXP_OP_TOK_LE: + return (delta <= 0); + case MAC_EXP_OP_TOK_GE: + return (delta >= 0); + case MAC_EXP_OP_TOK_GT: + return (delta > 0); + default: + msg_panic("%s: unknown operator: %d", + myname, tok_val); + } +} + +/* mac_exp_parse_error - report parse error, set error flag, return status */ + +static int PRINTFLIKE(2, 3) mac_exp_parse_error(MAC_EXP_CONTEXT *mc, + const char *fmt,...) +{ + va_list ap; + + va_start(ap, fmt); + vmsg_warn(fmt, ap); + va_end(ap); + return (mc->status |= MAC_PARSE_ERROR); +}; + +/* MAC_EXP_ERR_RETURN - report parse error, set error flag, return status */ + +#define MAC_EXP_ERR_RETURN(mc, fmt, ...) do { \ + return (mac_exp_parse_error(mc, fmt, __VA_ARGS__)); \ + } while (0) + + /* + * Postfix 3.0 introduces support for {text} operands. Only with these do we + * support the ternary ?: operator and relational operators. + * + * We cannot support operators in random text, because that would break Postfix + * 2.11 compatibility. For example, with the expression "${name?value}", the + * value is random text that may contain ':', '?', '{' and '}' characters. + * In particular, with Postfix 2.2 .. 2.11, "${name??foo:{b}ar}" evaluates + * to "?foo:{b}ar" or empty. There are explicit tests in this directory and + * the postconf directory to ensure that Postfix 2.11 compatibility is + * maintained. + * + * Ideally, future Postfix configurations enclose random text operands inside + * {} braces. These allow whitespace around operands, which improves + * readability. + */ + +/* MAC_EXP_FIND_LEFT_CURLY - skip over whitespace to '{', advance read ptr */ + +#define MAC_EXP_FIND_LEFT_CURLY(len, cp) \ + ((cp[len = strspn(cp, MAC_EXP_WHITESPACE)] == '{') ? \ + (cp += len) : 0) + +/* mac_exp_extract_curly_payload - balance {}, skip whitespace, return payload */ + +static char *mac_exp_extract_curly_payload(MAC_EXP_CONTEXT *mc, char **bp) +{ + char *payload; + char *cp; + int level; + int ch; + + /* + * Extract the payload and balance the {}. The caller is expected to skip + * leading whitespace before the {. See MAC_EXP_FIND_LEFT_CURLY(). + */ + for (level = 1, cp = *bp, payload = ++cp; /* see below */ ; cp++) { + if ((ch = *cp) == 0) { + mac_exp_parse_error(mc, "unbalanced {} in attribute expression: " + "\"%s\"", + *bp); + return (0); + } else if (ch == '{') { + level++; + } else if (ch == '}') { + if (--level <= 0) + break; + } + } + *cp++ = 0; + + /* + * Skip trailing whitespace after }. + */ + *bp = cp + strspn(cp, MAC_EXP_WHITESPACE); + return (payload); +} + +/* mac_exp_parse_relational - parse relational expression, advance read ptr */ + +static int mac_exp_parse_relational(MAC_EXP_CONTEXT *mc, const char **lookup, + char **bp) +{ + char *cp = *bp; + VSTRING *left_op_buf; + VSTRING *rite_op_buf; + const char *left_op_strval; + const char *rite_op_strval; + char *op_pos; + char *op_strval; + size_t op_len; + int op_tokval; + int op_result; + size_t tmp_len; + + /* + * Left operand. The caller is expected to skip leading whitespace before + * the {. See MAC_EXP_FIND_LEFT_CURLY(). + */ + if ((left_op_strval = mac_exp_extract_curly_payload(mc, &cp)) == 0) + return (mc->status); + + /* + * Operator. Todo: regexp operator. + */ + op_pos = cp; + op_len = strspn(cp, "<>!=?+-*/~&|%"); /* for better diagnostics. */ + op_strval = mystrndup(cp, op_len); + op_tokval = name_code(mac_exp_op_table, NAME_CODE_FLAG_NONE, op_strval); + myfree(op_strval); + if (op_tokval == MAC_EXP_OP_TOK_NONE) + MAC_EXP_ERR_RETURN(mc, "%s expected at: \"...%s}>>>%.20s\"", + MAC_EXP_OP_STR_ANY, left_op_strval, cp); + cp += op_len; + + /* + * Right operand. Todo: syntax may depend on operator. + */ + if (MAC_EXP_FIND_LEFT_CURLY(tmp_len, cp) == 0) + MAC_EXP_ERR_RETURN(mc, "\"{expression}\" expected at: " + "\"...{%s} %.*s>>>%.20s\"", + left_op_strval, (int) op_len, op_pos, cp); + if ((rite_op_strval = mac_exp_extract_curly_payload(mc, &cp)) == 0) + return (mc->status); + + /* + * Evaluate the relational expression. Todo: regexp support. + */ + mc->status |= + mac_expand(left_op_buf = vstring_alloc(100), left_op_strval, + mc->flags, mc->filter, mc->lookup, mc->context); + mc->status |= + mac_expand(rite_op_buf = vstring_alloc(100), rite_op_strval, + mc->flags, mc->filter, mc->lookup, mc->context); + op_result = mac_exp_eval(vstring_str(left_op_buf), op_tokval, + vstring_str(rite_op_buf)); + vstring_free(left_op_buf); + vstring_free(rite_op_buf); + if (mc->status & MAC_PARSE_ERROR) + return (mc->status); + + /* + * Here, we fake up a non-empty or empty parameter value lookup result, + * for compatibility with the historical code that looks named parameter + * values. + */ + *lookup = (op_result ? MAC_EXP_BVAL_TRUE : MAC_EXP_BVAL_FALSE); + *bp = cp; + return (0); +} + +/* mac_expand_callback - callback for mac_parse */ + +static int mac_expand_callback(int type, VSTRING *buf, void *ptr) +{ + static const char myname[] = "mac_expand_callback"; + MAC_EXP_CONTEXT *mc = (MAC_EXP_CONTEXT *) ptr; + int lookup_mode; + const char *lookup; + char *cp; + int ch; + ssize_t res_len; + ssize_t tmp_len; + const char *res_iftrue; + const char *res_iffalse; + + /* + * Sanity check. + */ + if (mc->level++ > 100) + mac_exp_parse_error(mc, "unreasonable macro call nesting: \"%s\"", + vstring_str(buf)); + if (mc->status & MAC_PARSE_ERROR) + return (mc->status); + + /* + * Named parameter or relational expression. In case of a syntax error, + * return without doing damage, and issue a warning instead. + */ + if (type == MAC_PARSE_EXPR) { + + cp = vstring_str(buf); + + /* + * Relational expression. If recursion is disabled, perform only one + * level of $name expansion. + */ + if (MAC_EXP_FIND_LEFT_CURLY(tmp_len, cp)) { + if (mac_exp_parse_relational(mc, &lookup, &cp) != 0) + return (mc->status); + + /* + * Look for the ? or : operator. + */ + if ((ch = *cp) != 0) { + if (ch != '?' && ch != ':') + MAC_EXP_ERR_RETURN(mc, "\"?\" or \":\" expected at: " + "\"...}>>>%.20s\"", cp); + cp++; + } + } + + /* + * Named parameter. + */ + else { + char *start; + + /* + * Look for the ? or : operator. In case of a syntax error, + * return without doing damage, and issue a warning instead. + */ + start = (cp += strspn(cp, MAC_EXP_WHITESPACE)); + for ( /* void */ ; /* void */ ; cp++) { + if ((ch = cp[tmp_len = strspn(cp, MAC_EXP_WHITESPACE)]) == 0) { + *cp = 0; + lookup_mode = MAC_EXP_MODE_USE; + break; + } + if (ch == '?' || ch == ':') { + *cp++ = 0; + cp += tmp_len; + lookup_mode = MAC_EXP_MODE_TEST; + break; + } + ch = *cp; + if (!ISALNUM(ch) && ch != '_') { + MAC_EXP_ERR_RETURN(mc, "attribute name syntax error at: " + "\"...%.*s>>>%.20s\"", + (int) (cp - vstring_str(buf)), + vstring_str(buf), cp); + } + } + + /* + * Look up the named parameter. Todo: allow the lookup function + * to specify if the result is safe for $name expanson. + */ + lookup = mc->lookup(start, lookup_mode, mc->context); + } + + /* + * Return the requested result. After parsing the result operand + * following ?, we fall through to parse the result operand following + * :. This is necessary with the ternary ?: operator: first, with + * MAC_EXP_FLAG_SCAN to parse both result operands with mac_parse(), + * and second, to find garbage after any result operand. Without + * MAC_EXP_FLAG_SCAN the content of only one of the ?: result + * operands will be parsed with mac_parse(); syntax errors in the + * other operand will be missed. + */ + switch (ch) { + case '?': + if (MAC_EXP_FIND_LEFT_CURLY(tmp_len, cp)) { + if ((res_iftrue = mac_exp_extract_curly_payload(mc, &cp)) == 0) + return (mc->status); + } else { + res_iftrue = cp; + cp = ""; /* no left-over text */ + } + if ((lookup != 0 && *lookup != 0) || (mc->flags & MAC_EXP_FLAG_SCAN)) + mc->status |= mac_parse(res_iftrue, mac_expand_callback, + (void *) mc); + if (*cp == 0) /* end of input, OK */ + break; + if (*cp != ':') /* garbage */ + MAC_EXP_ERR_RETURN(mc, "\":\" expected at: " + "\"...%s}>>>%.20s\"", res_iftrue, cp); + cp += 1; + /* FALLTHROUGH: do not remove, see comment above. */ + case ':': + if (MAC_EXP_FIND_LEFT_CURLY(tmp_len, cp)) { + if ((res_iffalse = mac_exp_extract_curly_payload(mc, &cp)) == 0) + return (mc->status); + } else { + res_iffalse = cp; + cp = ""; /* no left-over text */ + } + if (lookup == 0 || *lookup == 0 || (mc->flags & MAC_EXP_FLAG_SCAN)) + mc->status |= mac_parse(res_iffalse, mac_expand_callback, + (void *) mc); + if (*cp != 0) /* garbage */ + MAC_EXP_ERR_RETURN(mc, "unexpected input at: " + "\"...%s}>>>%.20s\"", res_iffalse, cp); + break; + case 0: + if (lookup == 0) { + mc->status |= MAC_PARSE_UNDEF; + } else if (*lookup == 0 || (mc->flags & MAC_EXP_FLAG_SCAN)) { + /* void */ ; + } else if (mc->flags & MAC_EXP_FLAG_RECURSE) { + vstring_strcpy(buf, lookup); + mc->status |= mac_parse(vstring_str(buf), mac_expand_callback, + (void *) mc); + } else { + res_len = VSTRING_LEN(mc->result); + vstring_strcat(mc->result, lookup); + if (mc->flags & MAC_EXP_FLAG_PRINTABLE) { + printable(vstring_str(mc->result) + res_len, '_'); + } else if (mc->filter) { + cp = vstring_str(mc->result) + res_len; + while (*(cp += strspn(cp, mc->filter))) + *cp++ = '_'; + } + } + break; + default: + msg_panic("%s: unknown operator code %d", myname, ch); + } + } + + /* + * Literal text. + */ + else if ((mc->flags & MAC_EXP_FLAG_SCAN) == 0) { + vstring_strcat(mc->result, vstring_str(buf)); + } + mc->level--; + + return (mc->status); +} + +/* mac_expand - expand $name instances */ + +int mac_expand(VSTRING *result, const char *pattern, int flags, + const char *filter, + MAC_EXP_LOOKUP_FN lookup, void *context) +{ + MAC_EXP_CONTEXT mc; + int status; + + /* + * Bundle up the request and do the substitutions. + */ + mc.result = result; + mc.flags = flags; + mc.filter = filter; + mc.lookup = lookup; + mc.context = context; + mc.status = 0; + mc.level = 0; + if ((flags & (MAC_EXP_FLAG_APPEND | MAC_EXP_FLAG_SCAN)) == 0) + VSTRING_RESET(result); + status = mac_parse(pattern, mac_expand_callback, (void *) &mc); + if ((flags & MAC_EXP_FLAG_SCAN) == 0) + VSTRING_TERMINATE(result); + + return (status); +} + +#ifdef TEST + + /* + * This code certainly deserves a stand-alone test program. + */ +#include <stdlib.h> +#include <stringops.h> +#include <htable.h> +#include <vstream.h> +#include <vstring_vstream.h> + +static const char *lookup(const char *name, int unused_mode, void *context) +{ + HTABLE *table = (HTABLE *) context; + + return (htable_find(table, name)); +} + +int main(int unused_argc, char **unused_argv) +{ + VSTRING *buf = vstring_alloc(100); + VSTRING *result = vstring_alloc(100); + char *cp; + char *name; + char *value; + HTABLE *table; + int stat; + + while (!vstream_feof(VSTREAM_IN)) { + + table = htable_create(0); + + /* + * Read a block of definitions, terminated with an empty line. + */ + while (vstring_get_nonl(buf, VSTREAM_IN) != VSTREAM_EOF) { + vstream_printf("<< %s\n", vstring_str(buf)); + vstream_fflush(VSTREAM_OUT); + if (VSTRING_LEN(buf) == 0) + break; + cp = vstring_str(buf); + name = mystrtok(&cp, CHARS_SPACE "="); + value = mystrtok(&cp, CHARS_SPACE "="); + htable_enter(table, name, value ? mystrdup(value) : 0); + } + + /* + * Read a block of patterns, terminated with an empty line or EOF. + */ + while (vstring_get_nonl(buf, VSTREAM_IN) != VSTREAM_EOF) { + vstream_printf("<< %s\n", vstring_str(buf)); + vstream_fflush(VSTREAM_OUT); + if (VSTRING_LEN(buf) == 0) + break; + cp = vstring_str(buf); + VSTRING_RESET(result); + stat = mac_expand(result, vstring_str(buf), MAC_EXP_FLAG_NONE, + (char *) 0, lookup, (void *) table); + vstream_printf("stat=%d result=%s\n", stat, vstring_str(result)); + vstream_fflush(VSTREAM_OUT); + } + htable_free(table, myfree); + vstream_printf("\n"); + } + + /* + * Clean up. + */ + vstring_free(buf); + vstring_free(result); + exit(0); +} + +#endif |