xref: /freebsd/contrib/ntp/sntp/libopts/nested.c (revision af6a5351a1fdb1130f18be6c782c4d48916eb971)
1 
2 /**
3  * \file nested.c
4  *
5  *  Handle options with arguments that contain nested values.
6  *
7  * @addtogroup autoopts
8  * @{
9  */
10 /*
11  *   Automated Options Nested Values module.
12  *
13  *  This file is part of AutoOpts, a companion to AutoGen.
14  *  AutoOpts is free software.
15  *  AutoOpts is Copyright (C) 1992-2015 by Bruce Korb - all rights reserved
16  *
17  *  AutoOpts is available under any one of two licenses.  The license
18  *  in use must be one of these two and the choice is under the control
19  *  of the user of the license.
20  *
21  *   The GNU Lesser General Public License, version 3 or later
22  *      See the files "COPYING.lgplv3" and "COPYING.gplv3"
23  *
24  *   The Modified Berkeley Software Distribution License
25  *      See the file "COPYING.mbsd"
26  *
27  *  These files have the following sha256 sums:
28  *
29  *  8584710e9b04216a394078dc156b781d0b47e1729104d666658aecef8ee32e95  COPYING.gplv3
30  *  4379e7444a0e2ce2b12dd6f5a52a27a4d02d39d247901d3285c88cf0d37f477b  COPYING.lgplv3
31  *  13aa749a5b0a454917a944ed8fffc530b784f5ead522b1aacaf4ec8aa55a6239  COPYING.mbsd
32  */
33 
34 typedef struct {
35     int     xml_ch;
36     int     xml_len;
37     char    xml_txt[8];
38 } xml_xlate_t;
39 
40 static xml_xlate_t const xml_xlate[] = {
41     { '&', 4, "amp;"  },
42     { '<', 3, "lt;"   },
43     { '>', 3, "gt;"   },
44     { '"', 5, "quot;" },
45     { '\'',5, "apos;" }
46 };
47 
48 #ifndef ENOMSG
49 #define ENOMSG ENOENT
50 #endif
51 
52 /* = = = START-STATIC-FORWARD = = = */
53 static void
54 remove_continuation(char * src);
55 
56 static char const *
57 scan_q_str(char const * pzTxt);
58 
59 static tOptionValue *
60 add_string(void ** pp, char const * name, size_t nm_len,
61            char const * val, size_t d_len);
62 
63 static tOptionValue *
64 add_bool(void ** pp, char const * name, size_t nm_len,
65          char const * val, size_t d_len);
66 
67 static tOptionValue *
68 add_number(void ** pp, char const * name, size_t nm_len,
69            char const * val, size_t d_len);
70 
71 static tOptionValue *
72 add_nested(void ** pp, char const * name, size_t nm_len,
73            char * val, size_t d_len);
74 
75 static char const *
76 scan_name(char const * name, tOptionValue * res);
77 
78 static char const *
79 unnamed_xml(char const * txt);
80 
81 static char const *
82 scan_xml_name(char const * name, size_t * nm_len, tOptionValue * val);
83 
84 static char const *
85 find_end_xml(char const * src, size_t nm_len, char const * val, size_t * len);
86 
87 static char const *
88 scan_xml(char const * xml_name, tOptionValue * res_val);
89 
90 static void
91 sort_list(tArgList * arg_list);
92 /* = = = END-STATIC-FORWARD = = = */
93 
94 /**
95  *  Backslashes are used for line continuations.  We keep the newline
96  *  characters, but trim out the backslash:
97  */
98 static void
99 remove_continuation(char * src)
100 {
101     char * pzD;
102 
103     do  {
104         while (*src == NL)  src++;
105         pzD = strchr(src, NL);
106         if (pzD == NULL)
107             return;
108 
109         /*
110          *  pzD has skipped at least one non-newline character and now
111          *  points to a newline character.  It now becomes the source and
112          *  pzD goes to the previous character.
113          */
114         src = pzD--;
115         if (*pzD != '\\')
116             pzD++;
117     } while (pzD == src);
118 
119     /*
120      *  Start shifting text.
121      */
122     for (;;) {
123         char ch = ((*pzD++) = *(src++));
124         switch (ch) {
125         case NUL:  return;
126         case '\\':
127             if (*src == NL)
128                 --pzD; /* rewrite on next iteration */
129         }
130     }
131 }
132 
133 /**
134  *  Find the end of a quoted string, skipping escaped quote characters.
135  */
136 static char const *
137 scan_q_str(char const * pzTxt)
138 {
139     char q = *(pzTxt++); /* remember the type of quote */
140 
141     for (;;) {
142         char ch = *(pzTxt++);
143         if (ch == NUL)
144             return pzTxt-1;
145 
146         if (ch == q)
147             return pzTxt;
148 
149         if (ch == '\\') {
150             ch = *(pzTxt++);
151             /*
152              *  IF the next character is NUL, drop the backslash, too.
153              */
154             if (ch == NUL)
155                 return pzTxt - 2;
156 
157             /*
158              *  IF the quote character or the escape character were escaped,
159              *  then skip both, as long as the string does not end.
160              */
161             if ((ch == q) || (ch == '\\')) {
162                 if (*(pzTxt++) == NUL)
163                     return pzTxt-1;
164             }
165         }
166     }
167 }
168 
169 
170 /**
171  *  Associate a name with either a string or no value.
172  *
173  * @param[in,out] pp        argument list to add to
174  * @param[in]     name      the name of the "suboption"
175  * @param[in]     nm_len    the length of the name
176  * @param[in]     val       the string value for the suboption
177  * @param[in]     d_len     the length of the value
178  *
179  * @returns the new value structure
180  */
181 static tOptionValue *
182 add_string(void ** pp, char const * name, size_t nm_len,
183            char const * val, size_t d_len)
184 {
185     tOptionValue * pNV;
186     size_t sz = nm_len + d_len + sizeof(*pNV);
187 
188     pNV = AGALOC(sz, "option name/str value pair");
189 
190     if (val == NULL) {
191         pNV->valType = OPARG_TYPE_NONE;
192         pNV->pzName = pNV->v.strVal;
193 
194     } else {
195         pNV->valType = OPARG_TYPE_STRING;
196         if (d_len > 0) {
197             char const * src = val;
198             char * pzDst = pNV->v.strVal;
199             int    ct    = (int)d_len;
200             do  {
201                 int ch = *(src++) & 0xFF;
202                 if (ch == NUL) goto data_copy_done;
203                 if (ch == '&')
204                     ch = get_special_char(&src, &ct);
205                 *(pzDst++) = (char)ch;
206             } while (--ct > 0);
207         data_copy_done:
208             *pzDst = NUL;
209 
210         } else {
211             pNV->v.strVal[0] = NUL;
212         }
213 
214         pNV->pzName = pNV->v.strVal + d_len + 1;
215     }
216 
217     memcpy(pNV->pzName, name, nm_len);
218     pNV->pzName[ nm_len ] = NUL;
219     addArgListEntry(pp, pNV);
220     return pNV;
221 }
222 
223 /**
224  *  Associate a name with a boolean value
225  *
226  * @param[in,out] pp        argument list to add to
227  * @param[in]     name      the name of the "suboption"
228  * @param[in]     nm_len    the length of the name
229  * @param[in]     val       the boolean value for the suboption
230  * @param[in]     d_len     the length of the value
231  *
232  * @returns the new value structure
233  */
234 static tOptionValue *
235 add_bool(void ** pp, char const * name, size_t nm_len,
236          char const * val, size_t d_len)
237 {
238     size_t sz = nm_len + sizeof(tOptionValue) + 1;
239     tOptionValue * new_val = AGALOC(sz, "bool val");
240 
241     /*
242      * Scan over whitespace is constrained by "d_len"
243      */
244     while (IS_WHITESPACE_CHAR(*val) && (d_len > 0)) {
245         d_len--; val++;
246     }
247 
248     if (d_len == 0)
249         new_val->v.boolVal = 0;
250 
251     else if (IS_DEC_DIGIT_CHAR(*val))
252         new_val->v.boolVal = (unsigned)atoi(val);
253 
254     else new_val->v.boolVal = ! IS_FALSE_TYPE_CHAR(*val);
255 
256     new_val->valType = OPARG_TYPE_BOOLEAN;
257     new_val->pzName = (char *)(new_val + 1);
258     memcpy(new_val->pzName, name, nm_len);
259     new_val->pzName[ nm_len ] = NUL;
260     addArgListEntry(pp, new_val);
261     return new_val;
262 }
263 
264 /**
265  *  Associate a name with strtol() value, defaulting to zero.
266  *
267  * @param[in,out] pp        argument list to add to
268  * @param[in]     name      the name of the "suboption"
269  * @param[in]     nm_len    the length of the name
270  * @param[in]     val       the numeric value for the suboption
271  * @param[in]     d_len     the length of the value
272  *
273  * @returns the new value structure
274  */
275 static tOptionValue *
276 add_number(void ** pp, char const * name, size_t nm_len,
277            char const * val, size_t d_len)
278 {
279     size_t sz = nm_len + sizeof(tOptionValue) + 1;
280     tOptionValue * new_val = AGALOC(sz, "int val");
281 
282     /*
283      * Scan over whitespace is constrained by "d_len"
284      */
285     while (IS_WHITESPACE_CHAR(*val) && (d_len > 0)) {
286         d_len--; val++;
287     }
288     if (d_len == 0)
289         new_val->v.longVal = 0;
290     else
291         new_val->v.longVal = strtol(val, 0, 0);
292 
293     new_val->valType = OPARG_TYPE_NUMERIC;
294     new_val->pzName  = (char *)(new_val + 1);
295     memcpy(new_val->pzName, name, nm_len);
296     new_val->pzName[ nm_len ] = NUL;
297     addArgListEntry(pp, new_val);
298     return new_val;
299 }
300 
301 /**
302  *  Associate a name with a nested/hierarchical value.
303  *
304  * @param[in,out] pp        argument list to add to
305  * @param[in]     name      the name of the "suboption"
306  * @param[in]     nm_len    the length of the name
307  * @param[in]     val       the nested values for the suboption
308  * @param[in]     d_len     the length of the value
309  *
310  * @returns the new value structure
311  */
312 static tOptionValue *
313 add_nested(void ** pp, char const * name, size_t nm_len,
314            char * val, size_t d_len)
315 {
316     tOptionValue * new_val;
317 
318     if (d_len == 0) {
319         size_t sz = nm_len + sizeof(*new_val) + 1;
320         new_val = AGALOC(sz, "empty nest");
321         new_val->v.nestVal = NULL;
322         new_val->valType = OPARG_TYPE_HIERARCHY;
323         new_val->pzName = (char *)(new_val + 1);
324         memcpy(new_val->pzName, name, nm_len);
325         new_val->pzName[ nm_len ] = NUL;
326 
327     } else {
328         new_val = optionLoadNested(val, name, nm_len);
329     }
330 
331     if (new_val != NULL)
332         addArgListEntry(pp, new_val);
333 
334     return new_val;
335 }
336 
337 /**
338  *  We have an entry that starts with a name.  Find the end of it, cook it
339  *  (if called for) and create the name/value association.
340  */
341 static char const *
342 scan_name(char const * name, tOptionValue * res)
343 {
344     tOptionValue * new_val;
345     char const *   pzScan = name+1; /* we know first char is a name char */
346     char const *   pzVal;
347     size_t         nm_len = 1;
348     size_t         d_len = 0;
349 
350     /*
351      *  Scan over characters that name a value.  These names may not end
352      *  with a colon, but they may contain colons.
353      */
354     pzScan = SPN_VALUE_NAME_CHARS(name + 1);
355     if (pzScan[-1] == ':')
356         pzScan--;
357     nm_len = (size_t)(pzScan - name);
358 
359     pzScan = SPN_HORIZ_WHITE_CHARS(pzScan);
360 
361  re_switch:
362 
363     switch (*pzScan) {
364     case '=':
365     case ':':
366         pzScan = SPN_HORIZ_WHITE_CHARS(pzScan + 1);
367         if ((*pzScan == '=') || (*pzScan == ':'))
368             goto default_char;
369         goto re_switch;
370 
371     case NL:
372     case ',':
373         pzScan++;
374         /* FALLTHROUGH */
375 
376     case NUL:
377         add_string(&(res->v.nestVal), name, nm_len, NULL, (size_t)0);
378         break;
379 
380     case '"':
381     case '\'':
382         pzVal = pzScan;
383         pzScan = scan_q_str(pzScan);
384         d_len = (size_t)(pzScan - pzVal);
385         new_val = add_string(&(res->v.nestVal), name, nm_len, pzVal,
386                          d_len);
387         if ((new_val != NULL) && (option_load_mode == OPTION_LOAD_COOKED))
388             ao_string_cook(new_val->v.strVal, NULL);
389         break;
390 
391     default:
392     default_char:
393         /*
394          *  We have found some strange text value.  It ends with a newline
395          *  or a comma.
396          */
397         pzVal = pzScan;
398         for (;;) {
399             char ch = *(pzScan++);
400             switch (ch) {
401             case NUL:
402                 pzScan--;
403                 d_len = (size_t)(pzScan - pzVal);
404                 goto string_done;
405                 /* FALLTHROUGH */
406 
407             case NL:
408                 if (   (pzScan > pzVal + 2)
409                     && (pzScan[-2] == '\\')
410                     && (pzScan[ 0] != NUL))
411                     continue;
412                 /* FALLTHROUGH */
413 
414             case ',':
415                 d_len = (size_t)(pzScan - pzVal) - 1;
416             string_done:
417                 new_val = add_string(&(res->v.nestVal), name, nm_len,
418                                      pzVal, d_len);
419                 if (new_val != NULL)
420                     remove_continuation(new_val->v.strVal);
421                 goto leave_scan_name;
422             }
423         }
424         break;
425     } leave_scan_name:;
426 
427     return pzScan;
428 }
429 
430 /**
431  * Some xml element that does not start with a name.
432  * The next character must be either '!' (introducing a comment),
433  * or '?' (introducing an XML meta-marker of some sort).
434  * We ignore these and indicate an error (NULL result) otherwise.
435  *
436  * @param[in] txt  the text within an xml bracket
437  * @returns the address of the character after the closing marker, or NULL.
438  */
439 static char const *
440 unnamed_xml(char const * txt)
441 {
442     switch (*txt) {
443     default:
444         txt = NULL;
445         break;
446 
447     case '!':
448         txt = strstr(txt, "-->");
449         if (txt != NULL)
450             txt += 3;
451         break;
452 
453     case '?':
454         txt = strchr(txt, '>');
455         if (txt != NULL)
456             txt++;
457         break;
458     }
459     return txt;
460 }
461 
462 /**
463  *  Scan off the xml element name, and the rest of the header, too.
464  *  Set the value type to NONE if it ends with "/>".
465  *
466  * @param[in]  name    the first name character (alphabetic)
467  * @param[out] nm_len  the length of the name
468  * @param[out] val     set valType field to STRING or NONE.
469  *
470  * @returns the scan resumption point, or NULL on error
471  */
472 static char const *
473 scan_xml_name(char const * name, size_t * nm_len, tOptionValue * val)
474 {
475     char const * scan = SPN_VALUE_NAME_CHARS(name + 1);
476     *nm_len = (size_t)(scan - name);
477     if (*nm_len > 64)
478         return NULL;
479     val->valType = OPARG_TYPE_STRING;
480 
481     if (IS_WHITESPACE_CHAR(*scan)) {
482         /*
483          * There are attributes following the name.  Parse 'em.
484          */
485         scan = SPN_WHITESPACE_CHARS(scan);
486         scan = parse_attrs(NULL, scan, &option_load_mode, val);
487         if (scan == NULL)
488             return NULL; /* oops */
489     }
490 
491     if (! IS_END_XML_TOKEN_CHAR(*scan))
492         return NULL; /* oops */
493 
494     if (*scan == '/') {
495         /*
496          * Single element XML entries get inserted as an empty string.
497          */
498         if (*++scan != '>')
499             return NULL;
500         val->valType = OPARG_TYPE_NONE;
501     }
502     return scan+1;
503 }
504 
505 /**
506  * We've found a closing '>' without a preceding '/', thus we must search
507  * the text for '<name/>' where "name" is the name of the XML element.
508  *
509  * @param[in]  name     the start of the name in the element header
510  * @param[in]  nm_len   the length of that name
511  * @param[out] len      the length of the value (string between header and
512  *                      the trailer/tail.
513  * @returns the character after the trailer, or NULL if not found.
514  */
515 static char const *
516 find_end_xml(char const * src, size_t nm_len, char const * val, size_t * len)
517 {
518     char z[72] = "</";
519     char * dst = z + 2;
520 
521     do  {
522         *(dst++) = *(src++);
523     } while (--nm_len > 0); /* nm_len is known to be 64 or less */
524     *(dst++) = '>';
525     *dst = NUL;
526 
527     {
528         char const * res = strstr(val, z);
529 
530         if (res != NULL) {
531             char const * end = (option_load_mode != OPTION_LOAD_KEEP)
532                 ? SPN_WHITESPACE_BACK(val, res)
533                 : res;
534             *len = (size_t)(end - val); /* includes trailing white space */
535             res =  SPN_WHITESPACE_CHARS(res + (dst - z));
536         }
537         return res;
538     }
539 }
540 
541 /**
542  *  We've found a '<' character.  We ignore this if it is a comment or a
543  *  directive.  If it is something else, then whatever it is we are looking
544  *  at is bogus.  Returning NULL stops processing.
545  *
546  * @param[in]     xml_name  the name of an xml bracket (usually)
547  * @param[in,out] res_val   the option data derived from the XML element
548  *
549  * @returns the place to resume scanning input
550  */
551 static char const *
552 scan_xml(char const * xml_name, tOptionValue * res_val)
553 {
554     size_t          nm_len, v_len;
555     char const *    scan;
556     char const *    val_str;
557     tOptionValue    valu;
558     tOptionLoadMode save_mode = option_load_mode;
559 
560     if (! IS_VAR_FIRST_CHAR(*++xml_name))
561         return unnamed_xml(xml_name);
562 
563     /*
564      * "scan_xml_name()" may change "option_load_mode".
565      */
566     val_str = scan_xml_name(xml_name, &nm_len, &valu);
567     if (val_str == NULL)
568         goto bail_scan_xml;
569 
570     if (valu.valType == OPARG_TYPE_NONE)
571         scan = val_str;
572     else {
573         if (option_load_mode != OPTION_LOAD_KEEP)
574             val_str = SPN_WHITESPACE_CHARS(val_str);
575         scan = find_end_xml(xml_name, nm_len, val_str, &v_len);
576         if (scan == NULL)
577             goto bail_scan_xml;
578     }
579 
580     /*
581      * "scan" now points to where the scan is to resume after returning.
582      * It either points after "/>" at the end of the XML element header,
583      * or it points after the "</name>" tail based on the name in the header.
584      */
585 
586     switch (valu.valType) {
587     case OPARG_TYPE_NONE:
588         add_string(&(res_val->v.nestVal), xml_name, nm_len, NULL, 0);
589         break;
590 
591     case OPARG_TYPE_STRING:
592     {
593         tOptionValue * new_val = add_string(
594             &(res_val->v.nestVal), xml_name, nm_len, val_str, v_len);
595 
596         if (option_load_mode != OPTION_LOAD_KEEP)
597             munge_str(new_val->v.strVal, option_load_mode);
598 
599         break;
600     }
601 
602     case OPARG_TYPE_BOOLEAN:
603         add_bool(&(res_val->v.nestVal), xml_name, nm_len, val_str, v_len);
604         break;
605 
606     case OPARG_TYPE_NUMERIC:
607         add_number(&(res_val->v.nestVal), xml_name, nm_len, val_str, v_len);
608         break;
609 
610     case OPARG_TYPE_HIERARCHY:
611     {
612         char * pz = AGALOC(v_len+1, "h scan");
613         memcpy(pz, val_str, v_len);
614         pz[v_len] = NUL;
615         add_nested(&(res_val->v.nestVal), xml_name, nm_len, pz, v_len);
616         AGFREE(pz);
617         break;
618     }
619 
620     case OPARG_TYPE_ENUMERATION:
621     case OPARG_TYPE_MEMBERSHIP:
622     default:
623         break;
624     }
625 
626     option_load_mode = save_mode;
627     return scan;
628 
629 bail_scan_xml:
630     option_load_mode = save_mode;
631     return NULL;
632 }
633 
634 
635 /**
636  *  Deallocate a list of option arguments.  This must have been gotten from
637  *  a hierarchical option argument, not a stacked list of strings.  It is
638  *  an internal call, so it is not validated.  The caller is responsible for
639  *  knowing what they are doing.
640  */
641 LOCAL void
642 unload_arg_list(tArgList * arg_list)
643 {
644     int ct = arg_list->useCt;
645     char const ** pnew_val = arg_list->apzArgs;
646 
647     while (ct-- > 0) {
648         tOptionValue * new_val = (tOptionValue *)VOIDP(*(pnew_val++));
649         if (new_val->valType == OPARG_TYPE_HIERARCHY)
650             unload_arg_list(new_val->v.nestVal);
651         AGFREE(new_val);
652     }
653 
654     AGFREE(arg_list);
655 }
656 
657 /*=export_func  optionUnloadNested
658  *
659  * what:  Deallocate the memory for a nested value
660  * arg:   + tOptionValue const * + pOptVal + the hierarchical value +
661  *
662  * doc:
663  *  A nested value needs to be deallocated.  The pointer passed in should
664  *  have been gotten from a call to @code{configFileLoad()} (See
665  *  @pxref{libopts-configFileLoad}).
666 =*/
667 void
668 optionUnloadNested(tOptionValue const * opt_val)
669 {
670     if (opt_val == NULL) return;
671     if (opt_val->valType != OPARG_TYPE_HIERARCHY) {
672         errno = EINVAL;
673         return;
674     }
675 
676     unload_arg_list(opt_val->v.nestVal);
677 
678     AGFREE(opt_val);
679 }
680 
681 /**
682  *  This is a _stable_ sort.  The entries are sorted alphabetically,
683  *  but within entries of the same name the ordering is unchanged.
684  *  Typically, we also hope the input is sorted.
685  */
686 static void
687 sort_list(tArgList * arg_list)
688 {
689     int ix;
690     int lm = arg_list->useCt;
691 
692     /*
693      *  This loop iterates "useCt" - 1 times.
694      */
695     for (ix = 0; ++ix < lm;) {
696         int iy = ix-1;
697         tOptionValue * new_v = C(tOptionValue *, arg_list->apzArgs[ix]);
698         tOptionValue * old_v = C(tOptionValue *, arg_list->apzArgs[iy]);
699 
700         /*
701          *  For as long as the new entry precedes the "old" entry,
702          *  move the old pointer.  Stop before trying to extract the
703          *  "-1" entry.
704          */
705         while (strcmp(old_v->pzName, new_v->pzName) > 0) {
706             arg_list->apzArgs[iy+1] = VOIDP(old_v);
707             old_v = (tOptionValue *)VOIDP(arg_list->apzArgs[--iy]);
708             if (iy < 0)
709                 break;
710         }
711 
712         /*
713          *  Always store the pointer.  Sometimes it is redundant,
714          *  but the redundancy is cheaper than a test and branch sequence.
715          */
716         arg_list->apzArgs[iy+1] = VOIDP(new_v);
717     }
718 }
719 
720 /*=
721  * private:
722  *
723  * what:  parse a hierarchical option argument
724  * arg:   + char const * + pzTxt  + the text to scan      +
725  * arg:   + char const * + pzName + the name for the text +
726  * arg:   + size_t       + nm_len + the length of "name"  +
727  *
728  * ret_type:  tOptionValue *
729  * ret_desc:  An allocated, compound value structure
730  *
731  * doc:
732  *  A block of text represents a series of values.  It may be an
733  *  entire configuration file, or it may be an argument to an
734  *  option that takes a hierarchical value.
735  *
736  *  If NULL is returned, errno will be set:
737  *  @itemize @bullet
738  *  @item
739  *  @code{EINVAL} the input text was NULL.
740  *  @item
741  *  @code{ENOMEM} the storage structures could not be allocated
742  *  @item
743  *  @code{ENOMSG} no configuration values were found
744  *  @end itemize
745 =*/
746 LOCAL tOptionValue *
747 optionLoadNested(char const * text, char const * name, size_t nm_len)
748 {
749     tOptionValue * res_val;
750 
751     /*
752      *  Make sure we have some data and we have space to put what we find.
753      */
754     if (text == NULL) {
755         errno = EINVAL;
756         return NULL;
757     }
758     text = SPN_WHITESPACE_CHARS(text);
759     if (*text == NUL) {
760         errno = ENOMSG;
761         return NULL;
762     }
763     res_val = AGALOC(sizeof(*res_val) + nm_len + 1, "nest args");
764     res_val->valType = OPARG_TYPE_HIERARCHY;
765     res_val->pzName  = (char *)(res_val + 1);
766     memcpy(res_val->pzName, name, nm_len);
767     res_val->pzName[nm_len] = NUL;
768 
769     {
770         tArgList * arg_list = AGALOC(sizeof(*arg_list), "nest arg l");
771 
772         res_val->v.nestVal = arg_list;
773         arg_list->useCt   = 0;
774         arg_list->allocCt = MIN_ARG_ALLOC_CT;
775     }
776 
777     /*
778      *  Scan until we hit a NUL.
779      */
780     do  {
781         text = SPN_WHITESPACE_CHARS(text);
782         if (IS_VAR_FIRST_CHAR(*text))
783             text = scan_name(text, res_val);
784 
785         else switch (*text) {
786         case NUL: goto scan_done;
787         case '<': text = scan_xml(text, res_val);
788                   if (text == NULL) goto woops;
789                   if (*text == ',') text++; break;
790         case '#': text = strchr(text, NL);  break;
791         default:  goto woops;
792         }
793     } while (text != NULL); scan_done:;
794 
795     {
796         tArgList * al = res_val->v.nestVal;
797         if (al->useCt == 0) {
798             errno = ENOMSG;
799             goto woops;
800         }
801         if (al->useCt > 1)
802             sort_list(al);
803     }
804 
805     return res_val;
806 
807  woops:
808     AGFREE(res_val->v.nestVal);
809     AGFREE(res_val);
810     return NULL;
811 }
812 
813 /*=export_func  optionNestedVal
814  * private:
815  *
816  * what:  parse a hierarchical option argument
817  * arg:   + tOptions * + opts + program options descriptor +
818  * arg:   + tOptDesc * + od   + the descriptor for this arg +
819  *
820  * doc:
821  *  Nested value was found on the command line
822 =*/
823 void
824 optionNestedVal(tOptions * opts, tOptDesc * od)
825 {
826     if (opts < OPTPROC_EMIT_LIMIT)
827         return;
828 
829     if (od->fOptState & OPTST_RESET) {
830         tArgList *    arg_list = od->optCookie;
831         int           ct;
832         char const ** av;
833 
834         if (arg_list == NULL)
835             return;
836         ct = arg_list->useCt;
837         av = arg_list->apzArgs;
838 
839         while (--ct >= 0) {
840             void * p = VOIDP(*(av++));
841             optionUnloadNested((tOptionValue const *)p);
842         }
843 
844         AGFREE(od->optCookie);
845 
846     } else {
847         tOptionValue * opt_val = optionLoadNested(
848             od->optArg.argString, od->pz_Name, strlen(od->pz_Name));
849 
850         if (opt_val != NULL)
851             addArgListEntry(&(od->optCookie), VOIDP(opt_val));
852     }
853 }
854 
855 /**
856  * get_special_char
857  */
858 LOCAL int
859 get_special_char(char const ** ppz, int * ct)
860 {
861     char const * pz = *ppz;
862     char *       rz;
863 
864     if (*ct < 3)
865         return '&';
866 
867     if (*pz == '#') {
868         int base = 10;
869         int retch;
870 
871         pz++;
872         if (*pz == 'x') {
873             base = 16;
874             pz++;
875         }
876         retch = (int)strtoul(pz, &rz, base);
877         pz = rz;
878         if (*pz != ';')
879             return '&';
880         base = (int)(++pz - *ppz);
881         if (base > *ct)
882             return '&';
883 
884         *ct -= base;
885         *ppz = pz;
886         return retch;
887     }
888 
889     {
890         int ctr = sizeof(xml_xlate) / sizeof(xml_xlate[0]);
891         xml_xlate_t const * xlatp = xml_xlate;
892 
893         for (;;) {
894             if (  (*ct >= xlatp->xml_len)
895                && (strncmp(pz, xlatp->xml_txt, (size_t)xlatp->xml_len) == 0)) {
896                 *ppz += xlatp->xml_len;
897                 *ct  -= xlatp->xml_len;
898                 return xlatp->xml_ch;
899             }
900 
901             if (--ctr <= 0)
902                 break;
903             xlatp++;
904         }
905     }
906     return '&';
907 }
908 
909 /**
910  * emit_special_char
911  */
912 LOCAL void
913 emit_special_char(FILE * fp, int ch)
914 {
915     int ctr = sizeof(xml_xlate) / sizeof(xml_xlate[0]);
916     xml_xlate_t const * xlatp = xml_xlate;
917 
918     putc('&', fp);
919     for (;;) {
920         if (ch == xlatp->xml_ch) {
921             fputs(xlatp->xml_txt, fp);
922             return;
923         }
924         if (--ctr <= 0)
925             break;
926         xlatp++;
927     }
928     fprintf(fp, XML_HEX_BYTE_FMT, (ch & 0xFF));
929 }
930 
931 /** @}
932  *
933  * Local Variables:
934  * mode: C
935  * c-file-style: "stroustrup"
936  * indent-tabs-mode: nil
937  * End:
938  * end of autoopts/nested.c */
939