1 /*++
2 /* NAME
3 /*	attr_scan_plain 3
4 /* SUMMARY
5 /*	recover attributes from byte stream
6 /* SYNOPSIS
7 /*	#include <attr.h>
8 /*
9 /*	int	attr_scan_plain(fp, flags, type, name, ..., ATTR_TYPE_END)
10 /*	VSTREAM	*fp;
11 /*	int	flags;
12 /*	int	type;
13 /*	char	*name;
14 /*
15 /*	int	attr_vscan_plain(fp, flags, ap)
16 /*	VSTREAM	*fp;
17 /*	int	flags;
18 /*	va_list	ap;
19 /*
20 /*	int	attr_scan_more_plain(fp)
21 /*	VSTREAM *fp;
22 /* DESCRIPTION
23 /*	attr_scan_plain() takes zero or more (name, value) request attributes
24 /*	and recovers the attribute values from the byte stream that was
25 /*	possibly generated by attr_print_plain().
26 /*
27 /*	attr_vscan_plain() provides an alternative interface that is convenient
28 /*	for calling from within a variadic function.
29 /*
30 /*	attr_scan_more_plain() returns 0 when a terminator is found
31 /*	(and consumes that terminator), returns 1 when more input
32 /*	is expected (without consuming input), and returns -1
33 /*	otherwise (error).
34 /*
35 /*	The input stream is formatted as follows, where (item)* stands
36 /*	for zero or more instances of the specified item, and where
37 /*	(item1 | item2) stands for choice:
38 /*
39 /* .in +5
40 /*	attr-list :== (simple-attr | multi-attr)* newline
41 /* .br
42 /*	multi-attr :== "{" newline simple-attr* "}" newline
43 /* .br
44 /*	simple-attr :== attr-name "=" attr-value newline
45 /* .br
46 /*	attr-name :== any string without null or "=" or newline.
47 /* .br
48 /*	attr-value :== any string without null or newline.
49 /* .br
50 /*	newline :== the ASCII newline character
51 /* .in
52 /*
53 /*	All attribute names and attribute values are sent as plain
54 /*	strings. Each string must be no longer than 4*var_line_limit
55 /*	characters. The formatting rules aim to make implementations in PERL
56 /*	and other languages easy.
57 /*
58 /*	Normally, attributes must be received in the sequence as specified
59 /*	with the attr_scan_plain() argument list.  The input stream may
60 /*	contain additional attributes at any point in the input stream,
61 /*	including additional instances of requested attributes.
62 /*
63 /*	Additional input attributes or input attribute instances are silently
64 /*	skipped over, unless the ATTR_FLAG_EXTRA processing flag is specified
65 /*	(see below). This allows for some flexibility in the evolution of
66 /*	protocols while still providing the option of being strict where
67 /*	this is desirable.
68 /*
69 /*	Arguments:
70 /* .IP fp
71 /*	Stream to recover the input attributes from.
72 /* .IP flags
73 /*	The bit-wise OR of zero or more of the following.
74 /* .RS
75 /* .IP ATTR_FLAG_MISSING
76 /*	Log a warning when the input attribute list terminates before all
77 /*	requested attributes are recovered. It is always an error when the
78 /*	input stream ends without the newline attribute list terminator.
79 /* .IP ATTR_FLAG_EXTRA
80 /*	Log a warning and stop attribute recovery when the input stream
81 /*	contains an attribute that was not requested. This includes the
82 /*	case of additional instances of a requested attribute.
83 /* .IP ATTR_FLAG_MORE
84 /*	After recovering the requested attributes, leave the input stream
85 /*	in a state that is usable for more attr_scan_plain() operations
86 /*	from the same input attribute list.
87 /*	By default, attr_scan_plain() skips forward past the input attribute
88 /*	list terminator.
89 /* .IP ATTR_FLAG_PRINTABLE
90 /*	Santize received string values with printable(_, '?').
91 /* .IP ATTR_FLAG_STRICT
92 /*	For convenience, this value combines both ATTR_FLAG_MISSING and
93 /*	ATTR_FLAG_EXTRA.
94 /* .IP ATTR_FLAG_NONE
95 /*	For convenience, this value requests none of the above.
96 /* .RE
97 /* .IP List of attributes followed by terminator:
98 /* .RS
99 /* .IP "RECV_ATTR_INT(const char *name, int *ptr)"
100 /*	This argument is followed by an attribute name and an integer pointer.
101 /* .IP "RECV_ATTR_LONG(const char *name, long *ptr)"
102 /*	This argument is followed by an attribute name and a long pointer.
103 /* .IP "RECV_ATTR_STR(const char *name, VSTRING *vp)"
104 /*	This argument is followed by an attribute name and a VSTRING pointer.
105 /* .IP "RECV_ATTR_STREQ(const char *name, const char *value)"
106 /*	The name and value must match what the client sends.
107 /*	This attribute does not increment the result value.
108 /* .IP "RECV_ATTR_DATA(const char *name, VSTRING *vp)"
109 /*	This argument is followed by an attribute name and a VSTRING pointer.
110 /* .IP "RECV_ATTR_FUNC(ATTR_SCAN_CUSTOM_FN, void *data)"
111 /*	This argument is followed by a function pointer and a generic data
112 /*	pointer. The caller-specified function returns < 0 in case of
113 /*	error.
114 /* .IP "RECV_ATTR_HASH(HTABLE *table)"
115 /* .IP "RECV_ATTR_NAMEVAL(NVTABLE *table)"
116 /*	Receive a sequence of attribute names and string values.
117 /*	There can be no more than 1024 attributes in a hash table.
118 /* .sp
119 /*	The attribute string values are stored in the hash table under
120 /*	keys equal to the attribute name (obtained from the input stream).
121 /*	Values from the input stream are added to the hash table. Existing
122 /*	hash table entries are not replaced.
123 /* .sp
124 /*	Note: the SEND_ATTR_HASH or SEND_ATTR_NAMEVAL requests
125 /*	format their payload as a multi-attr sequence (see syntax
126 /*	above). When the receiver's input does not start with a
127 /*	multi-attr delimiter (i.e. the sender did not request
128 /*	SEND_ATTR_HASH or SEND_ATTR_NAMEVAL), the receiver will
129 /*	store all attribute names and values up to the attribute
130 /*	list terminator. In terms of code, this means that the
131 /*	RECV_ATTR_HASH or RECV_ATTR_NAMEVAL request must be followed
132 /*	by ATTR_TYPE_END.
133 /* .IP ATTR_TYPE_END
134 /*	This argument terminates the requested attribute list.
135 /* .RE
136 /* BUGS
137 /*	RECV_ATTR_HASH (RECV_ATTR_NAMEVAL) accepts attributes with arbitrary
138 /*	names from possibly untrusted sources.
139 /*	This is unsafe, unless the resulting table is queried only with
140 /*	known to be good attribute names.
141 /* DIAGNOSTICS
142 /*	attr_scan_plain() and attr_vscan_plain() return -1 when malformed input
143 /*	is detected (string too long, incomplete line, missing end marker).
144 /*	Otherwise, the result value is the number of attributes that were
145 /*	successfully recovered from the input stream (a hash table counts
146 /*	as the number of entries stored into the table).
147 /*
148 /*	Panic: interface violation. All system call errors are fatal.
149 /* SEE ALSO
150 /*	attr_print_plain(3) send attributes over byte stream.
151 /* LICENSE
152 /* .ad
153 /* .fi
154 /*	The Secure Mailer license must be distributed with this software.
155 /* AUTHOR(S)
156 /*	Wietse Venema
157 /*	IBM T.J. Watson Research
158 /*	P.O. Box 704
159 /*	Yorktown Heights, NY 10598, USA
160 /*
161 /*	Wietse Venema
162 /*	Google, Inc.
163 /*	111 8th Avenue
164 /*	New York, NY 10011, USA
165 /*--*/
166 
167 /* System library. */
168 
169 #include <sys_defs.h>
170 #include <stdarg.h>
171 #include <string.h>
172 #include <stdio.h>
173 
174 /* Utility library. */
175 
176 #include <msg.h>
177 #include <mymalloc.h>
178 #include <vstream.h>
179 #include <vstring.h>
180 #include <htable.h>
181 #include <base64_code.h>
182 #include <stringops.h>
183 #include <attr.h>
184 
185 /* Application specific. */
186 
187 #define STR(x)	vstring_str(x)
188 #define LEN(x)	VSTRING_LEN(x)
189 
190 /* attr_scan_plain_string - pull a string from the input stream */
191 
attr_scan_plain_string(VSTREAM * fp,VSTRING * plain_buf,int terminator,const char * context)192 static int attr_scan_plain_string(VSTREAM *fp, VSTRING *plain_buf,
193 				        int terminator, const char *context)
194 {
195 #if 0
196     extern int var_line_limit;		/* XXX */
197     int     limit = var_line_limit * 4;
198 
199 #endif
200     int     ch;
201 
202     VSTRING_RESET(plain_buf);
203     while ((ch = VSTREAM_GETC(fp)) != '\n'
204 	   && (terminator == 0 || ch != terminator)) {
205 	if (ch == VSTREAM_EOF) {
206 	    msg_warn("%s on %s while reading %s",
207 		vstream_ftimeout(fp) ? "timeout" : "premature end-of-input",
208 		     VSTREAM_PATH(fp), context);
209 	    return (-1);
210 	}
211 	VSTRING_ADDCH(plain_buf, ch);
212 #if 0
213 	if (LEN(plain_buf) > limit) {
214 	    msg_warn("string length > %d characters from %s while reading %s",
215 		     limit, VSTREAM_PATH(fp), context);
216 	    return (-1);
217 	}
218 #endif
219     }
220     VSTRING_TERMINATE(plain_buf);
221 
222     if (msg_verbose)
223 	msg_info("%s: %s", context, *STR(plain_buf) ? STR(plain_buf) : "(end)");
224     return (ch);
225 }
226 
227 /* attr_scan_plain_data - pull a data blob from the input stream */
228 
attr_scan_plain_data(VSTREAM * fp,VSTRING * str_buf,int terminator,const char * context)229 static int attr_scan_plain_data(VSTREAM *fp, VSTRING *str_buf,
230 				        int terminator,
231 				        const char *context)
232 {
233     static VSTRING *base64_buf = 0;
234     int     ch;
235 
236     if (base64_buf == 0)
237 	base64_buf = vstring_alloc(10);
238     if ((ch = attr_scan_plain_string(fp, base64_buf, terminator, context)) < 0)
239 	return (-1);
240     if (base64_decode(str_buf, STR(base64_buf), LEN(base64_buf)) == 0) {
241 	msg_warn("malformed base64 data from %s while reading %s: %.100s",
242 		 VSTREAM_PATH(fp), context, STR(base64_buf));
243 	return (-1);
244     }
245     return (ch);
246 }
247 
248 /* attr_scan_plain_number - pull a number from the input stream */
249 
attr_scan_plain_number(VSTREAM * fp,unsigned * ptr,VSTRING * str_buf,int terminator,const char * context)250 static int attr_scan_plain_number(VSTREAM *fp, unsigned *ptr, VSTRING *str_buf,
251 				        int terminator, const char *context)
252 {
253     char    junk = 0;
254     int     ch;
255 
256     if ((ch = attr_scan_plain_string(fp, str_buf, terminator, context)) < 0)
257 	return (-1);
258     if (sscanf(STR(str_buf), "%u%c", ptr, &junk) != 1 || junk != 0) {
259 	msg_warn("malformed numerical data from %s while reading %s: %.100s",
260 		 VSTREAM_PATH(fp), context, STR(str_buf));
261 	return (-1);
262     }
263     return (ch);
264 }
265 
266 /* attr_scan_plain_long_number - pull a number from the input stream */
267 
attr_scan_plain_long_number(VSTREAM * fp,unsigned long * ptr,VSTRING * str_buf,int terminator,const char * context)268 static int attr_scan_plain_long_number(VSTREAM *fp, unsigned long *ptr,
269 				               VSTRING *str_buf,
270 				               int terminator,
271 				               const char *context)
272 {
273     char    junk = 0;
274     int     ch;
275 
276     if ((ch = attr_scan_plain_string(fp, str_buf, terminator, context)) < 0)
277 	return (-1);
278     if (sscanf(STR(str_buf), "%lu%c", ptr, &junk) != 1 || junk != 0) {
279 	msg_warn("malformed numerical data from %s while reading %s: %.100s",
280 		 VSTREAM_PATH(fp), context, STR(str_buf));
281 	return (-1);
282     }
283     return (ch);
284 }
285 
286 /* attr_vscan_plain - receive attribute list from stream */
287 
attr_vscan_plain(VSTREAM * fp,int flags,va_list ap)288 int     attr_vscan_plain(VSTREAM *fp, int flags, va_list ap)
289 {
290     const char *myname = "attr_scan_plain";
291     static VSTRING *str_buf = 0;
292     static VSTRING *name_buf = 0;
293     int     wanted_type = -1;
294     char   *wanted_name;
295     unsigned int *number;
296     unsigned long *long_number;
297     VSTRING *string;
298     HTABLE *hash_table;
299     int     ch;
300     int     conversions;
301     ATTR_SCAN_CUSTOM_FN scan_fn;
302     void   *scan_arg;
303     const char *expect_val;
304 
305     /*
306      * Sanity check.
307      */
308     if (flags & ~ATTR_FLAG_ALL)
309 	msg_panic("%s: bad flags: 0x%x", myname, flags);
310 
311     /*
312      * EOF check.
313      */
314     if ((ch = VSTREAM_GETC(fp)) == VSTREAM_EOF)
315 	return (0);
316     vstream_ungetc(fp, ch);
317 
318     /*
319      * Initialize.
320      */
321     if (str_buf == 0) {
322 	str_buf = vstring_alloc(10);
323 	name_buf = vstring_alloc(10);
324     }
325 
326     /*
327      * Iterate over all (type, name, value) triples.
328      */
329     for (conversions = 0; /* void */ ; conversions++) {
330 
331 	/*
332 	 * Determine the next attribute type and attribute name on the
333 	 * caller's wish list.
334 	 *
335 	 * If we're reading into a hash table, we already know that the
336 	 * attribute value is string-valued, and we get the attribute name
337 	 * from the input stream instead. This is secure only when the
338 	 * resulting table is queried with known to be good attribute names.
339 	 */
340 	if (wanted_type != ATTR_TYPE_HASH
341 	    && wanted_type != ATTR_TYPE_CLOSE) {
342 	    wanted_type = va_arg(ap, int);
343 	    if (wanted_type == ATTR_TYPE_END) {
344 		if ((flags & ATTR_FLAG_MORE) != 0)
345 		    return (conversions);
346 		wanted_name = "(list terminator)";
347 	    } else if (wanted_type == ATTR_TYPE_HASH) {
348 		wanted_name = "(any attribute name or list terminator)";
349 		hash_table = va_arg(ap, HTABLE *);
350 	    } else if (wanted_type != ATTR_TYPE_FUNC) {
351 		wanted_name = va_arg(ap, char *);
352 	    }
353 	}
354 
355 	/*
356 	 * Locate the next attribute of interest in the input stream.
357 	 */
358 	while (wanted_type != ATTR_TYPE_FUNC) {
359 
360 	    /*
361 	     * Get the name of the next attribute. Hitting EOF is always bad.
362 	     * Hitting the end-of-input early is OK if the caller is prepared
363 	     * to deal with missing inputs.
364 	     */
365 	    if (msg_verbose)
366 		msg_info("%s: wanted attribute: %s",
367 			 VSTREAM_PATH(fp), wanted_name);
368 	    if ((ch = attr_scan_plain_string(fp, name_buf, '=',
369 				    "input attribute name")) == VSTREAM_EOF)
370 		return (-1);
371 	    if (ch == '\n' && LEN(name_buf) == 0) {
372 		if (wanted_type == ATTR_TYPE_END
373 		    || wanted_type == ATTR_TYPE_HASH)
374 		    return (conversions);
375 		if ((flags & ATTR_FLAG_MISSING) != 0)
376 		    msg_warn("missing attribute %s in input from %s",
377 			     wanted_name, VSTREAM_PATH(fp));
378 		return (conversions);
379 	    }
380 
381 	    /*
382 	     * See if the caller asks for this attribute.
383 	     */
384 	    if (wanted_type == ATTR_TYPE_HASH
385 	      && ch == '\n' && strcmp(ATTR_NAME_OPEN, STR(name_buf)) == 0) {
386 		wanted_type = ATTR_TYPE_CLOSE;
387 		wanted_name = "(any attribute name or '}')";
388 		/* Advance in the input stream. */
389 		continue;
390 	    } else if (wanted_type == ATTR_TYPE_CLOSE
391 	     && ch == '\n' && strcmp(ATTR_NAME_CLOSE, STR(name_buf)) == 0) {
392 		/* Advance in the argument list. */
393 		wanted_type = -1;
394 		break;
395 	    }
396 	    if (wanted_type == ATTR_TYPE_HASH
397 		|| wanted_type == ATTR_TYPE_CLOSE
398 		|| (wanted_type != ATTR_TYPE_END
399 		    && strcmp(wanted_name, STR(name_buf)) == 0))
400 		break;
401 	    if ((flags & ATTR_FLAG_EXTRA) != 0) {
402 		msg_warn("unexpected attribute %s from %s (expecting: %s)",
403 			 STR(name_buf), VSTREAM_PATH(fp), wanted_name);
404 		return (conversions);
405 	    }
406 
407 	    /*
408 	     * Skip over this attribute. The caller does not ask for it.
409 	     */
410 	    while (ch != '\n' && (ch = VSTREAM_GETC(fp)) != VSTREAM_EOF)
411 		 /* void */ ;
412 	}
413 
414 	/*
415 	 * Do the requested conversion.
416 	 */
417 	switch (wanted_type) {
418 	case ATTR_TYPE_INT:
419 	    if (ch != '=') {
420 		msg_warn("missing value for number attribute %s from %s",
421 			 STR(name_buf), VSTREAM_PATH(fp));
422 		return (-1);
423 	    }
424 	    number = va_arg(ap, unsigned int *);
425 	    if ((ch = attr_scan_plain_number(fp, number, str_buf, 0,
426 					     "input attribute value")) < 0)
427 		return (-1);
428 	    break;
429 	case ATTR_TYPE_LONG:
430 	    if (ch != '=') {
431 		msg_warn("missing value for number attribute %s from %s",
432 			 STR(name_buf), VSTREAM_PATH(fp));
433 		return (-1);
434 	    }
435 	    long_number = va_arg(ap, unsigned long *);
436 	    if ((ch = attr_scan_plain_long_number(fp, long_number, str_buf,
437 					   0, "input attribute value")) < 0)
438 		return (-1);
439 	    break;
440 	case ATTR_TYPE_STR:
441 	    if (ch != '=') {
442 		msg_warn("missing value for string attribute %s from %s",
443 			 STR(name_buf), VSTREAM_PATH(fp));
444 		return (-1);
445 	    }
446 	    string = va_arg(ap, VSTRING *);
447 	    if ((ch = attr_scan_plain_string(fp, string, 0,
448 					     "input attribute value")) < 0)
449 		return (-1);
450 	    if (flags & ATTR_FLAG_PRINTABLE)
451 		(void) printable(STR(string), '?');
452 	    break;
453 	case ATTR_TYPE_DATA:
454 	    if (ch != '=') {
455 		msg_warn("missing value for data attribute %s from %s",
456 			 STR(name_buf), VSTREAM_PATH(fp));
457 		return (-1);
458 	    }
459 	    string = va_arg(ap, VSTRING *);
460 	    if ((ch = attr_scan_plain_data(fp, string, 0,
461 					   "input attribute value")) < 0)
462 		return (-1);
463 	    break;
464 	case ATTR_TYPE_FUNC:
465 	    scan_fn = va_arg(ap, ATTR_SCAN_CUSTOM_FN);
466 	    scan_arg = va_arg(ap, void *);
467 	    if (scan_fn(attr_scan_plain, fp, flags | ATTR_FLAG_MORE, scan_arg) < 0)
468 		return (-1);
469 	    break;
470 	case ATTR_TYPE_STREQ:
471 	    if (ch != '=') {
472 		msg_warn("missing value for string attribute %s from %s",
473 			 STR(name_buf), VSTREAM_PATH(fp));
474 		return (-1);
475 	    }
476 	    expect_val = va_arg(ap, const char *);
477 	    if ((ch = attr_scan_plain_string(fp, str_buf, 0,
478 					     "input attribute value")) < 0)
479 		return (-1);
480 	    if (strcmp(expect_val, STR(str_buf)) != 0) {
481 		msg_warn("unexpected %s %s from %s (expected: %s)",
482 			 STR(name_buf), STR(str_buf), VSTREAM_PATH(fp),
483 			 expect_val);
484 		return (-1);
485 	    }
486 	    conversions -= 1;
487 	    break;
488 	case ATTR_TYPE_HASH:
489 	case ATTR_TYPE_CLOSE:
490 	    if (ch != '=') {
491 		msg_warn("missing value for string attribute %s from %s",
492 			 STR(name_buf), VSTREAM_PATH(fp));
493 		return (-1);
494 	    }
495 	    if ((ch = attr_scan_plain_string(fp, str_buf, 0,
496 					     "input attribute value")) < 0)
497 		return (-1);
498 	    if (flags & ATTR_FLAG_PRINTABLE) {
499 		(void) printable(STR(name_buf), '?');
500 		(void) printable(STR(str_buf), '?');
501 	    }
502 	    if (htable_locate(hash_table, STR(name_buf)) != 0) {
503 		if ((flags & ATTR_FLAG_EXTRA) != 0) {
504 		    msg_warn("duplicate attribute %s in input from %s",
505 			     STR(name_buf), VSTREAM_PATH(fp));
506 		    return (conversions);
507 		}
508 	    } else if (hash_table->used >= ATTR_HASH_LIMIT) {
509 		msg_warn("attribute count exceeds limit %d in input from %s",
510 			 ATTR_HASH_LIMIT, VSTREAM_PATH(fp));
511 		return (conversions);
512 	    } else {
513 		htable_enter(hash_table, STR(name_buf),
514 			     mystrdup(STR(str_buf)));
515 	    }
516 	    break;
517 	case -1:
518 	    conversions -= 1;
519 	    break;
520 	default:
521 	    msg_panic("%s: unknown type code: %d", myname, wanted_type);
522 	}
523     }
524 }
525 
526 /* attr_scan_plain - read attribute list from stream */
527 
attr_scan_plain(VSTREAM * fp,int flags,...)528 int     attr_scan_plain(VSTREAM *fp, int flags,...)
529 {
530     va_list ap;
531     int     ret;
532 
533     va_start(ap, flags);
534     ret = attr_vscan_plain(fp, flags, ap);
535     va_end(ap);
536     return (ret);
537 }
538 
539 /* attr_scan_more_plain - look ahead for more */
540 
attr_scan_more_plain(VSTREAM * fp)541 int     attr_scan_more_plain(VSTREAM *fp)
542 {
543     int     ch;
544 
545     switch (ch = VSTREAM_GETC(fp)) {
546     case '\n':
547 	if (msg_verbose)
548 	    msg_info("%s: terminator (consumed)", VSTREAM_PATH(fp));
549 	return (0);
550     case VSTREAM_EOF:
551 	if (msg_verbose)
552 	    msg_info("%s: EOF", VSTREAM_PATH(fp));
553 	return (-1);
554     default:
555 	if (msg_verbose)
556 	    msg_info("%s: non-terminator '%c' (lookahead)",
557 		     VSTREAM_PATH(fp), ch);
558 	(void) vstream_ungetc(fp, ch);
559 	return (1);
560     }
561 }
562 
563 #ifdef TEST
564 
565  /*
566   * Proof of concept test program.  Mirror image of the attr_scan_plain test
567   * program.
568   */
569 #include <msg_vstream.h>
570 
571 int     var_line_limit = 2048;
572 
main(int unused_argc,char ** used_argv)573 int     main(int unused_argc, char **used_argv)
574 {
575     VSTRING *data_val = vstring_alloc(1);
576     VSTRING *str_val = vstring_alloc(1);
577     HTABLE *table = htable_create(1);
578     HTABLE_INFO **ht_info_list;
579     HTABLE_INFO **ht;
580     int     int_val;
581     long    long_val;
582     long    long_val2;
583     int     ret;
584 
585     msg_verbose = 1;
586     msg_vstream_init(used_argv[0], VSTREAM_ERR);
587     if ((ret = attr_scan_plain(VSTREAM_IN,
588 			       ATTR_FLAG_STRICT,
589 			       RECV_ATTR_STREQ("protocol", "test"),
590 			       RECV_ATTR_INT(ATTR_NAME_INT, &int_val),
591 			       RECV_ATTR_LONG(ATTR_NAME_LONG, &long_val),
592 			       RECV_ATTR_STR(ATTR_NAME_STR, str_val),
593 			       RECV_ATTR_DATA(ATTR_NAME_DATA, data_val),
594 			       RECV_ATTR_HASH(table),
595 			       RECV_ATTR_LONG(ATTR_NAME_LONG, &long_val2),
596 			       ATTR_TYPE_END)) > 4) {
597 	vstream_printf("%s %d\n", ATTR_NAME_INT, int_val);
598 	vstream_printf("%s %ld\n", ATTR_NAME_LONG, long_val);
599 	vstream_printf("%s %s\n", ATTR_NAME_STR, STR(str_val));
600 	vstream_printf("%s %s\n", ATTR_NAME_DATA, STR(data_val));
601 	ht_info_list = htable_list(table);
602 	for (ht = ht_info_list; *ht; ht++)
603 	    vstream_printf("(hash) %s %s\n", ht[0]->key, (char *) ht[0]->value);
604 	myfree((void *) ht_info_list);
605 	vstream_printf("%s %ld\n", ATTR_NAME_LONG, long_val2);
606     } else {
607 	vstream_printf("return: %d\n", ret);
608     }
609     if ((ret = attr_scan_plain(VSTREAM_IN,
610 			       ATTR_FLAG_STRICT,
611 			       RECV_ATTR_STREQ("protocol", "test"),
612 			       RECV_ATTR_INT(ATTR_NAME_INT, &int_val),
613 			       RECV_ATTR_LONG(ATTR_NAME_LONG, &long_val),
614 			       RECV_ATTR_STR(ATTR_NAME_STR, str_val),
615 			       RECV_ATTR_DATA(ATTR_NAME_DATA, data_val),
616 			       ATTR_TYPE_END)) == 4) {
617 	vstream_printf("%s %d\n", ATTR_NAME_INT, int_val);
618 	vstream_printf("%s %ld\n", ATTR_NAME_LONG, long_val);
619 	vstream_printf("%s %s\n", ATTR_NAME_STR, STR(str_val));
620 	vstream_printf("%s %s\n", ATTR_NAME_DATA, STR(data_val));
621 	ht_info_list = htable_list(table);
622 	for (ht = ht_info_list; *ht; ht++)
623 	    vstream_printf("(hash) %s %s\n", ht[0]->key, (char *) ht[0]->value);
624 	myfree((void *) ht_info_list);
625     } else {
626 	vstream_printf("return: %d\n", ret);
627     }
628     if ((ret = attr_scan_plain(VSTREAM_IN,
629 			       ATTR_FLAG_STRICT,
630 			       RECV_ATTR_STREQ("protocol", "test"),
631 			       ATTR_TYPE_END)) != 0)
632 	vstream_printf("return: %d\n", ret);
633     if (vstream_fflush(VSTREAM_OUT) != 0)
634 	msg_fatal("write error: %m");
635 
636     vstring_free(data_val);
637     vstring_free(str_val);
638     htable_free(table, myfree);
639 
640     return (0);
641 }
642 
643 #endif
644