1/****************************************************************************
2 * Copyright 2018-2019,2020 Thomas E. Dickey                                *
3 * Copyright 1998-2016,2017 Free Software Foundation, Inc.                  *
4 *                                                                          *
5 * Permission is hereby granted, free of charge, to any person obtaining a  *
6 * copy of this software and associated documentation files (the            *
7 * "Software"), to deal in the Software without restriction, including      *
8 * without limitation the rights to use, copy, modify, merge, publish,      *
9 * distribute, distribute with modifications, sublicense, and/or sell       *
10 * copies of the Software, and to permit persons to whom the Software is    *
11 * furnished to do so, subject to the following conditions:                 *
12 *                                                                          *
13 * The above copyright notice and this permission notice shall be included  *
14 * in all copies or substantial portions of the Software.                   *
15 *                                                                          *
16 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS  *
17 * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF               *
18 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.   *
19 * IN NO EVENT SHALL THE ABOVE COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,   *
20 * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR    *
21 * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR    *
22 * THE USE OR OTHER DEALINGS IN THE SOFTWARE.                               *
23 *                                                                          *
24 * Except as contained in this notice, the name(s) of the above copyright   *
25 * holders shall not be used in advertising or otherwise to promote the     *
26 * sale, use or other dealings in this Software without prior written       *
27 * authorization.                                                           *
28 ****************************************************************************/
29
30/****************************************************************************
31 *  Author: Zeyd M. Ben-Halim <zmbenhal@netcom.com> 1992,1995               *
32 *     and: Eric S. Raymond <esr@snark.thyrsus.com>                         *
33 *     and: Thomas E. Dickey                        1996-on                 *
34 ****************************************************************************/
35
36/*
37 *	comp_parse.c -- parser driver loop and use handling.
38 *
39 *	Use this code by calling _nc_read_entry_source() on as many source
40 *	files as you like (either terminfo or termcap syntax).  If you
41 *	want use-resolution, call _nc_resolve_uses2().  To free the list
42 *	storage, do _nc_free_entries().
43 */
44
45#include <curses.priv.h>
46
47#include <ctype.h>
48
49#include <tic.h>
50
51MODULE_ID("$Id: comp_parse.c,v 1.110 2020/02/29 15:46:00 anonymous.maarten Exp $")
52
53static void sanity_check2(TERMTYPE2 *, bool);
54NCURSES_IMPEXP void (NCURSES_API *_nc_check_termtype2) (TERMTYPE2 *, bool) = sanity_check2;
55
56static void fixup_acsc(TERMTYPE2 *, int);
57
58static void
59enqueue(ENTRY * ep)
60/* add an entry to the in-core list */
61{
62    ENTRY *newp = _nc_copy_entry(ep);
63
64    if (newp == 0)
65	_nc_err_abort(MSG_NO_MEMORY);
66
67    newp->last = _nc_tail;
68    _nc_tail = newp;
69
70    newp->next = 0;
71    if (newp->last)
72	newp->last->next = newp;
73}
74
75#define NAMEBUFFER_SIZE (MAX_NAME_SIZE + 2)
76
77static char *
78force_bar(char *dst, char *src)
79{
80    if (strchr(src, '|') == 0) {
81	size_t len = strlen(src);
82	if (len > MAX_NAME_SIZE)
83	    len = MAX_NAME_SIZE;
84	_nc_STRNCPY(dst, src, MAX_NAME_SIZE);
85	_nc_STRCPY(dst + len, "|", NAMEBUFFER_SIZE - len);
86	src = dst;
87    }
88    return src;
89}
90#define ForceBar(dst, src) ((strchr(src, '|') == 0) ? force_bar(dst, src) : src)
91
92#if NCURSES_USE_TERMCAP && NCURSES_XNAMES
93static char *
94skip_index(char *name)
95{
96    char *bar = strchr(name, '|');
97
98    if (bar != 0 && (bar - name) == 2)
99	name = bar + 1;
100
101    return name;
102}
103#endif
104
105static bool
106check_collisions(char *n1, char *n2, int counter)
107{
108    char *pstart, *qstart, *pend, *qend;
109    char nc1[NAMEBUFFER_SIZE];
110    char nc2[NAMEBUFFER_SIZE];
111
112    n1 = ForceBar(nc1, n1);
113    n2 = ForceBar(nc2, n2);
114
115#if NCURSES_USE_TERMCAP && NCURSES_XNAMES
116    if ((_nc_syntax == SYN_TERMCAP) && _nc_user_definable) {
117	n1 = skip_index(n1);
118	n2 = skip_index(n2);
119    }
120#endif
121
122    for (pstart = n1; (pend = strchr(pstart, '|')); pstart = pend + 1) {
123	for (qstart = n2; (qend = strchr(qstart, '|')); qstart = qend + 1) {
124	    if ((pend - pstart == qend - qstart)
125		&& memcmp(pstart, qstart, (size_t) (pend - pstart)) == 0) {
126		if (counter > 0)
127		    (void) fprintf(stderr, "Name collision '%.*s' between\n",
128				   (int) (pend - pstart), pstart);
129		return (TRUE);
130	    }
131	}
132    }
133
134    return (FALSE);
135}
136
137static char *
138next_name(char *name)
139{
140    if (*name != '\0')
141	++name;
142    return name;
143}
144
145static char *
146name_ending(char *name)
147{
148    if (*name == '\0') {
149	name = 0;
150    } else {
151	while (*name != '\0' && *name != '|')
152	    ++name;
153    }
154    return name;
155}
156
157/*
158 * Essentially, find the conflict reported in check_collisions() and remove
159 * it from the second name, unless that happens to be the last alias.
160 */
161static bool
162remove_collision(char *n1, char *n2)
163{
164    char *p2 = n2;
165    char *pstart, *qstart, *pend, *qend;
166    bool removed = FALSE;
167
168#if NCURSES_USE_TERMCAP && NCURSES_XNAMES
169    if ((_nc_syntax == SYN_TERMCAP) && _nc_user_definable) {
170	n1 = skip_index(n1);
171	p2 = n2 = skip_index(n2);
172    }
173#endif
174
175    for (pstart = n1; (pend = name_ending(pstart)); pstart = next_name(pend)) {
176	for (qstart = n2; (qend = name_ending(qstart)); qstart = next_name(qend)) {
177	    if ((pend - pstart == qend - qstart)
178		&& memcmp(pstart, qstart, (size_t) (pend - pstart)) == 0) {
179		if (qstart != p2 || *qend == '|') {
180		    if (*qend == '|')
181			++qend;
182		    while ((*qstart++ = *qend++) != '\0') ;
183		    fprintf(stderr, "...now\t%s\n", p2);
184		    removed = TRUE;
185		} else {
186		    fprintf(stderr, "Cannot remove alias '%.*s'\n",
187			    (int) (qend - qstart), qstart);
188		}
189		break;
190	    }
191	}
192    }
193
194    return removed;
195}
196
197/* do any of the aliases in a pair of terminal names match? */
198NCURSES_EXPORT(bool)
199_nc_entry_match(char *n1, char *n2)
200{
201    return check_collisions(n1, n2, 0);
202}
203
204/****************************************************************************
205 *
206 * Entry compiler and resolution logic
207 *
208 ****************************************************************************/
209
210NCURSES_EXPORT(void)
211_nc_read_entry_source(FILE *fp, char *buf,
212		      int literal, bool silent,
213		      bool(*hook) (ENTRY *))
214/* slurp all entries in the given file into core */
215{
216    ENTRY thisentry;
217    bool oldsuppress = _nc_suppress_warnings;
218    int immediate = 0;
219
220    if (silent)
221	_nc_suppress_warnings = TRUE;	/* shut the lexer up, too */
222
223    _nc_reset_input(fp, buf);
224    for (;;) {
225	memset(&thisentry, 0, sizeof(thisentry));
226	if (_nc_parse_entry(&thisentry, literal, silent) == ERR)
227	    break;
228	if (!isalnum(UChar(thisentry.tterm.term_names[0])))
229	    _nc_err_abort("terminal names must start with letter or digit");
230
231	/*
232	 * This can be used for immediate compilation of entries with no "use="
233	 * references to disk.  That avoids consuming a lot of memory when the
234	 * resolution code could fetch entries off disk.
235	 */
236	if (hook != NULLHOOK && (*hook) (&thisentry)) {
237	    immediate++;
238	} else {
239	    enqueue(&thisentry);
240	    /*
241	     * The enqueued entry is copied with _nc_copy_termtype(), so we can
242	     * free some of the data from thisentry, i.e., the arrays.
243	     */
244	    FreeIfNeeded(thisentry.tterm.Booleans);
245	    FreeIfNeeded(thisentry.tterm.Numbers);
246	    FreeIfNeeded(thisentry.tterm.Strings);
247#if NCURSES_XNAMES
248	    FreeIfNeeded(thisentry.tterm.ext_Names);
249#endif
250	}
251    }
252
253    if (_nc_tail) {
254	/* set up the head pointer */
255	for (_nc_head = _nc_tail; _nc_head->last; _nc_head = _nc_head->last)
256	    continue;
257
258	DEBUG(1, ("head = %s", _nc_head->tterm.term_names));
259	DEBUG(1, ("tail = %s", _nc_tail->tterm.term_names));
260    }
261#ifdef TRACE
262    else if (!immediate)
263	DEBUG(1, ("no entries parsed"));
264#endif
265
266    _nc_suppress_warnings = oldsuppress;
267}
268
269#if NCURSES_XNAMES
270static unsigned
271find_capname(TERMTYPE2 *p, const char *name)
272{
273    unsigned num_names = NUM_EXT_NAMES(p);
274    unsigned n;
275    if (name != 0) {
276	for (n = 0; n < num_names; ++n) {
277	    if (!strcmp(p->ext_Names[n], name))
278		break;
279	}
280    } else {
281	n = num_names + 1;
282    }
283    return n;
284}
285
286static int
287extended_captype(TERMTYPE2 *p, unsigned which)
288{
289    int result = UNDEF;
290    unsigned limit = 0;
291    limit += p->ext_Booleans;
292    if (limit != 0 && which < limit) {
293	result = BOOLEAN;
294    } else {
295	limit += p->ext_Numbers;
296	if (limit != 0 && which < limit) {
297	    result = NUMBER;
298	} else {
299	    limit += p->ext_Strings;
300	    if (limit != 0 && which < limit) {
301		result = STRING;
302	    } else if (which >= limit) {
303		result = CANCEL;
304	    }
305	}
306    }
307    return result;
308}
309
310static const char *
311name_of_captype(int which)
312{
313    const char *result = "?";
314    switch (which) {
315    case BOOLEAN:
316	result = "boolean";
317	break;
318    case NUMBER:
319	result = "number";
320	break;
321    case STRING:
322	result = "string";
323	break;
324    }
325    return result;
326}
327
328#define valid_TERMTYPE2(p) \
329	((p) != 0 && \
330	 (p)->term_names != 0 && \
331	 (p)->ext_Names != 0)
332
333/*
334 * Disallow changing the type of an extended capability when doing a "use"
335 * if one or the other is a string.
336 */
337static int
338invalid_merge(TERMTYPE2 *to, TERMTYPE2 *from)
339{
340    int rc = FALSE;
341    if (valid_TERMTYPE2(to)
342	&& valid_TERMTYPE2(from)) {
343	char *to_name = _nc_first_name(to->term_names);
344	char *from_name = strdup(_nc_first_name(from->term_names));
345	unsigned num_names = NUM_EXT_NAMES(from);
346	unsigned n;
347
348	for (n = 0; n < num_names; ++n) {
349	    const char *capname = from->ext_Names[n];
350	    int tt = extended_captype(to, find_capname(to, capname));
351	    int tf = extended_captype(from, n);
352
353	    if (tt <= STRING
354		&& tf <= STRING
355		&& (tt == STRING) != (tf == STRING)) {
356		if (from_name != 0 && strcmp(to_name, from_name)) {
357		    DEBUG(2,
358			  ("merge of %s to %s changes type of %s from %s to %s",
359			   from_name,
360			   to_name,
361			   from->ext_Names[n],
362			   name_of_captype(tf),
363			   name_of_captype(tt)));
364		} else {
365		    DEBUG(2, ("merge of %s changes type of %s from %s to %s",
366			      to_name,
367			      from->ext_Names[n],
368			      name_of_captype(tf),
369			      name_of_captype(tt)));
370		}
371		_nc_warning("merge changes type of %s from %s to %s",
372			    from->ext_Names[n],
373			    name_of_captype(tf),
374			    name_of_captype(tt));
375		rc = TRUE;
376	    }
377	}
378	free(from_name);
379    }
380    return rc;
381}
382#define validate_merge(p, q) \
383	if (invalid_merge(&((p)->tterm), &((q)->tterm))) \
384		return FALSE
385#else
386#define validate_merge(p, q)	/* nothing */
387#endif
388
389NCURSES_EXPORT(int)
390_nc_resolve_uses2(bool fullresolve, bool literal)
391/* try to resolve all use capabilities */
392{
393    ENTRY *qp, *rp, *lastread = 0;
394    bool keepgoing;
395    unsigned i;
396    int unresolved, total_unresolved, multiples;
397
398    DEBUG(2, ("RESOLUTION BEGINNING"));
399
400    /*
401     * Check for multiple occurrences of the same name.
402     */
403    multiples = 0;
404    for_entry_list(qp) {
405	int matchcount = 0;
406
407	for_entry_list(rp) {
408	    if (qp > rp
409		&& check_collisions(qp->tterm.term_names,
410				    rp->tterm.term_names,
411				    matchcount + 1)) {
412		if (!matchcount++) {
413		    (void) fprintf(stderr, "\t%s\n", rp->tterm.term_names);
414		}
415		(void) fprintf(stderr, "and\t%s\n", qp->tterm.term_names);
416		if (!remove_collision(rp->tterm.term_names,
417				      qp->tterm.term_names)) {
418		    ++multiples;
419		}
420	    }
421	}
422    }
423    if (multiples > 0)
424	return (FALSE);
425
426    DEBUG(2, ("NO MULTIPLE NAME OCCURRENCES"));
427
428    /*
429     * First resolution stage: compute link pointers corresponding to names.
430     */
431    total_unresolved = 0;
432    _nc_curr_col = -1;
433    for_entry_list(qp) {
434	unresolved = 0;
435	for (i = 0; i < qp->nuses; i++) {
436	    bool foundit;
437	    char *child = _nc_first_name(qp->tterm.term_names);
438	    char *lookfor = qp->uses[i].name;
439	    long lookline = qp->uses[i].line;
440
441	    if (lookfor == 0)
442		continue;
443
444	    foundit = FALSE;
445
446	    _nc_set_type(child);
447
448	    /* first, try to resolve from in-core records */
449	    for_entry_list(rp) {
450		if (rp != qp
451		    && _nc_name_match(rp->tterm.term_names, lookfor, "|")) {
452		    DEBUG(2, ("%s: resolving use=%s (in core)",
453			      child, lookfor));
454
455		    qp->uses[i].link = rp;
456		    foundit = TRUE;
457		}
458	    }
459
460	    /* if that didn't work, try to merge in a compiled entry */
461	    if (!foundit) {
462		TERMTYPE2 thisterm;
463		char filename[PATH_MAX];
464
465		memset(&thisterm, 0, sizeof(thisterm));
466		if (_nc_read_entry2(lookfor, filename, &thisterm) == 1) {
467		    DEBUG(2, ("%s: resolving use=%s (compiled)",
468			      child, lookfor));
469
470		    TYPE_MALLOC(ENTRY, 1, rp);
471		    rp->tterm = thisterm;
472		    rp->nuses = 0;
473		    rp->next = lastread;
474		    lastread = rp;
475
476		    qp->uses[i].link = rp;
477		    foundit = TRUE;
478		}
479	    }
480
481	    /* no good, mark this one unresolvable and complain */
482	    if (!foundit) {
483		unresolved++;
484		total_unresolved++;
485
486		_nc_curr_line = (int) lookline;
487		_nc_warning("resolution of use=%s failed", lookfor);
488		qp->uses[i].link = 0;
489	    }
490	}
491    }
492    if (total_unresolved) {
493	/* free entries read in off disk */
494	_nc_free_entries(lastread);
495	return (FALSE);
496    }
497
498    DEBUG(2, ("NAME RESOLUTION COMPLETED OK"));
499
500    /*
501     * OK, at this point all (char *) references in `name' members
502     * have been successfully converted to (ENTRY *) pointers in
503     * `link' members.  Time to do the actual merges.
504     */
505    if (fullresolve) {
506	do {
507	    ENTRY merged;
508
509	    keepgoing = FALSE;
510
511	    for_entry_list(qp) {
512		if (qp->nuses > 0) {
513		    DEBUG(2, ("%s: attempting merge",
514			      _nc_first_name(qp->tterm.term_names)));
515		    /*
516		     * If any of the use entries we're looking for is
517		     * incomplete, punt.  We'll catch this entry on a
518		     * subsequent pass.
519		     */
520		    for (i = 0; i < qp->nuses; i++)
521			if (qp->uses[i].link
522			    && qp->uses[i].link->nuses) {
523			    DEBUG(2, ("%s: use entry %d unresolved",
524				      _nc_first_name(qp->tterm.term_names), i));
525			    goto incomplete;
526			}
527
528		    /*
529		     * First, make sure there is no garbage in the
530		     * merge block.  As a side effect, copy into
531		     * the merged entry the name field and string
532		     * table pointer.
533		     */
534		    _nc_copy_termtype2(&(merged.tterm), &(qp->tterm));
535
536		    /*
537		     * Now merge in each use entry in the proper
538		     * (reverse) order.
539		     */
540		    for (; qp->nuses; qp->nuses--) {
541			validate_merge(&merged,
542				       qp->uses[qp->nuses - 1].link);
543			_nc_merge_entry(&merged,
544					qp->uses[qp->nuses - 1].link);
545		    }
546
547		    /*
548		     * Now merge in the original entry.
549		     */
550		    validate_merge(&merged, qp);
551		    _nc_merge_entry(&merged, qp);
552
553		    /*
554		     * Replace the original entry with the merged one.
555		     */
556		    FreeIfNeeded(qp->tterm.Booleans);
557		    FreeIfNeeded(qp->tterm.Numbers);
558		    FreeIfNeeded(qp->tterm.Strings);
559#if NCURSES_XNAMES
560		    FreeIfNeeded(qp->tterm.ext_Names);
561#endif
562		    qp->tterm = merged.tterm;
563		    _nc_wrap_entry(qp, TRUE);
564
565		    /*
566		     * We know every entry is resolvable because name resolution
567		     * didn't bomb.  So go back for another pass.
568		     */
569		    /* FALLTHRU */
570		  incomplete:
571		    keepgoing = TRUE;
572		}
573	    }
574	} while
575	    (keepgoing);
576
577	DEBUG(2, ("MERGES COMPLETED OK"));
578    }
579
580    /*
581     * We'd like to free entries read in off disk at this point, but can't.
582     * The merge_entry() code doesn't copy the strings in the use entries,
583     * it just aliases them.  If this ever changes, do a
584     * free_entries(lastread) here.
585     */
586
587    DEBUG(2, ("RESOLUTION FINISHED"));
588
589    if (fullresolve) {
590	_nc_curr_col = -1;
591	for_entry_list(qp) {
592	    _nc_curr_line = (int) qp->startline;
593	    _nc_set_type(_nc_first_name(qp->tterm.term_names));
594	    /*
595	     * tic overrides this function pointer to provide more verbose
596	     * checking.
597	     */
598	    if (_nc_check_termtype2 != sanity_check2) {
599		SCREEN *save_SP = SP;
600		SCREEN fake_sp;
601		TERMINAL fake_tm;
602		TERMINAL *save_tm = cur_term;
603
604		/*
605		 * Setup so that tic can use ordinary terminfo interface to
606		 * obtain capability information.
607		 */
608		memset(&fake_sp, 0, sizeof(fake_sp));
609		memset(&fake_tm, 0, sizeof(fake_tm));
610		fake_sp._term = &fake_tm;
611		TerminalType(&fake_tm) = qp->tterm;
612		_nc_set_screen(&fake_sp);
613		set_curterm(&fake_tm);
614
615		_nc_check_termtype2(&qp->tterm, literal);
616
617		_nc_set_screen(save_SP);
618		set_curterm(save_tm);
619	    } else {
620		fixup_acsc(&qp->tterm, literal);
621	    }
622	}
623	DEBUG(2, ("SANITY CHECK FINISHED"));
624    }
625
626    return (TRUE);
627}
628
629/*
630 * This bit of legerdemain turns all the terminfo variable names into
631 * references to locations in the arrays Booleans, Numbers, and Strings ---
632 * precisely what's needed.
633 */
634
635#undef CUR
636#define CUR tp->
637
638static void
639fixup_acsc(TERMTYPE2 *tp, int literal)
640{
641    if (!literal) {
642	if (acs_chars == ABSENT_STRING
643	    && PRESENT(enter_alt_charset_mode)
644	    && PRESENT(exit_alt_charset_mode))
645	    acs_chars = strdup(VT_ACSC);
646    }
647}
648
649static void
650sanity_check2(TERMTYPE2 *tp, bool literal)
651{
652    if (!PRESENT(exit_attribute_mode)) {
653#ifdef __UNUSED__		/* this casts too wide a net */
654	bool terminal_entry = !strchr(tp->term_names, '+');
655	if (terminal_entry &&
656	    (PRESENT(set_attributes)
657	     || PRESENT(enter_standout_mode)
658	     || PRESENT(enter_underline_mode)
659	     || PRESENT(enter_blink_mode)
660	     || PRESENT(enter_bold_mode)
661	     || PRESENT(enter_dim_mode)
662	     || PRESENT(enter_secure_mode)
663	     || PRESENT(enter_protected_mode)
664	     || PRESENT(enter_reverse_mode)))
665	    _nc_warning("no exit_attribute_mode");
666#endif /* __UNUSED__ */
667	PAIRED(enter_standout_mode, exit_standout_mode);
668	PAIRED(enter_underline_mode, exit_underline_mode);
669#if defined(enter_italics_mode) && defined(exit_italics_mode)
670	PAIRED(enter_italics_mode, exit_italics_mode);
671#endif
672    }
673
674    /* we do this check/fix in postprocess_termcap(), but some packagers
675     * prefer to bypass it...
676     */
677    if (!literal) {
678	fixup_acsc(tp, literal);
679	ANDMISSING(enter_alt_charset_mode, acs_chars);
680	ANDMISSING(exit_alt_charset_mode, acs_chars);
681    }
682
683    /* listed in structure-member order of first argument */
684    PAIRED(enter_alt_charset_mode, exit_alt_charset_mode);
685    ANDMISSING(enter_blink_mode, exit_attribute_mode);
686    ANDMISSING(enter_bold_mode, exit_attribute_mode);
687    PAIRED(exit_ca_mode, enter_ca_mode);
688    PAIRED(enter_delete_mode, exit_delete_mode);
689    ANDMISSING(enter_dim_mode, exit_attribute_mode);
690    PAIRED(enter_insert_mode, exit_insert_mode);
691    ANDMISSING(enter_secure_mode, exit_attribute_mode);
692    ANDMISSING(enter_protected_mode, exit_attribute_mode);
693    ANDMISSING(enter_reverse_mode, exit_attribute_mode);
694    PAIRED(from_status_line, to_status_line);
695    PAIRED(meta_off, meta_on);
696
697    PAIRED(prtr_on, prtr_off);
698    PAIRED(save_cursor, restore_cursor);
699    PAIRED(enter_xon_mode, exit_xon_mode);
700    PAIRED(enter_am_mode, exit_am_mode);
701    ANDMISSING(label_off, label_on);
702#if defined(display_clock) && defined(remove_clock)
703    PAIRED(display_clock, remove_clock);
704#endif
705    ANDMISSING(set_color_pair, initialize_pair);
706}
707
708#if NO_LEAKS
709NCURSES_EXPORT(void)
710_nc_leaks_tic(void)
711{
712    T((T_CALLED("_nc_free_tic()")));
713    _nc_globals.leak_checking = TRUE;
714    _nc_alloc_entry_leaks();
715    _nc_captoinfo_leaks();
716    _nc_comp_scan_leaks();
717#if BROKEN_LINKER || USE_REENTRANT
718    _nc_names_leaks();
719    _nc_codes_leaks();
720#endif
721    _nc_tic_expand(0, FALSE, 0);
722}
723
724NCURSES_EXPORT(void)
725_nc_free_tic(int code)
726{
727    _nc_leaks_tic();
728    exit_terminfo(code);
729}
730#endif
731