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