1 /****************************************************************************
2  * Copyright 2018-2022,2023 Thomas E. Dickey                                *
3  * Copyright 2006-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: Thomas E. Dickey                                                *
32  ****************************************************************************/
33 
34 /*
35  * Iterators for terminal databases.
36  */
37 
38 #include <curses.priv.h>
39 
40 #include <time.h>
41 #include <tic.h>
42 
43 #if USE_HASHED_DB
44 #include <hashed_db.h>
45 #endif
46 
47 MODULE_ID("$Id: db_iterator.c,v 1.50 2023/06/24 21:52:32 tom Exp $")
48 
49 #define HaveTicDirectory _nc_globals.have_tic_directory
50 #define KeepTicDirectory _nc_globals.keep_tic_directory
51 #define TicDirectory     _nc_globals.tic_directory
52 #define my_blob          _nc_globals.dbd_blob
53 #define my_list          _nc_globals.dbd_list
54 #define my_size          _nc_globals.dbd_size
55 #define my_time          _nc_globals.dbd_time
56 #define my_vars          _nc_globals.dbd_vars
57 
58 static void
add_to_blob(const char * text,size_t limit)59 add_to_blob(const char *text, size_t limit)
60 {
61     (void) limit;
62 
63     if (*text != '\0') {
64 	char *last = my_blob + strlen(my_blob);
65 	if (last != my_blob)
66 	    *last++ = NCURSES_PATHSEP;
67 	_nc_STRCPY(last, text, limit);
68     }
69 }
70 
71 static bool
check_existence(const char * name,struct stat * sb)72 check_existence(const char *name, struct stat *sb)
73 {
74     bool result = FALSE;
75 
76     if (quick_prefix(name)) {
77 	result = TRUE;
78     } else if (stat(name, sb) == 0
79 	       && (S_ISDIR(sb->st_mode)
80 		   || (S_ISREG(sb->st_mode) && sb->st_size))) {
81 	result = TRUE;
82     }
83 #if USE_HASHED_DB
84     else if (strlen(name) < PATH_MAX - sizeof(DBM_SUFFIX)) {
85 	char temp[PATH_MAX];
86 	_nc_SPRINTF(temp, _nc_SLIMIT(sizeof(temp)) "%s%s", name, DBM_SUFFIX);
87 	if (stat(temp, sb) == 0 && S_ISREG(sb->st_mode) && sb->st_size) {
88 	    result = TRUE;
89 	}
90     }
91 #endif
92     return result;
93 }
94 
95 /*
96  * Trim newlines (and backslashes preceding those) and tab characters to
97  * help simplify scripting of the quick-dump feature.  Leave spaces and
98  * other backslashes alone.
99  */
100 static void
trim_formatting(char * source)101 trim_formatting(char *source)
102 {
103     char *target = source;
104     char ch;
105 
106     while ((ch = *source++) != '\0') {
107 	if (ch == '\\' && *source == '\n')
108 	    continue;
109 	if (ch == '\n' || ch == '\t')
110 	    continue;
111 	*target++ = ch;
112     }
113     *target = '\0';
114 }
115 
116 /*
117  * Store the latest value of an environment variable in my_vars[] so we can
118  * detect if one changes, invalidating the cached search-list.
119  */
120 static bool
update_getenv(const char * name,DBDIRS which)121 update_getenv(const char *name, DBDIRS which)
122 {
123     bool result = FALSE;
124 
125     if (which < dbdLAST) {
126 	char *value;
127 	char *cached_value = my_vars[which].value;
128 	bool same_value;
129 
130 	if ((value = getenv(name)) != 0) {
131 	    value = strdup(value);
132 	}
133 	same_value = ((value == 0 && cached_value == 0) ||
134 		      (value != 0 &&
135 		       cached_value != 0 &&
136 		       strcmp(value, cached_value) == 0));
137 
138 	/* Set variable name to enable checks in cache_expired(). */
139 	my_vars[which].name = name;
140 
141 	if (!same_value) {
142 	    FreeIfNeeded(my_vars[which].value);
143 	    my_vars[which].value = value;
144 	    result = TRUE;
145 	} else {
146 	    free(value);
147 	}
148     }
149     return result;
150 }
151 
152 #if NCURSES_USE_DATABASE || NCURSES_USE_TERMCAP
153 static char *
cache_getenv(const char * name,DBDIRS which)154 cache_getenv(const char *name, DBDIRS which)
155 {
156     char *result = 0;
157 
158     (void) update_getenv(name, which);
159     if (which < dbdLAST) {
160 	result = my_vars[which].value;
161     }
162     return result;
163 }
164 #endif
165 
166 /*
167  * The cache expires if at least a second has passed since the initial lookup,
168  * or if one of the environment variables changed.
169  *
170  * Only a few applications use multiple lookups of terminal entries, seems that
171  * aside from bulk I/O such as tic and toe, that leaves interactive programs
172  * which should not be modifying the terminal databases in a way that would
173  * invalidate the search-list.
174  *
175  * The "1-second" is to allow for user-directed changes outside the program.
176  */
177 static bool
cache_expired(void)178 cache_expired(void)
179 {
180     bool result = FALSE;
181     time_t now = time((time_t *) 0);
182 
183     if (now > my_time) {
184 	result = TRUE;
185     } else {
186 	DBDIRS n;
187 	for (n = (DBDIRS) 0; n < dbdLAST; ++n) {
188 	    if (my_vars[n].name != 0
189 		&& update_getenv(my_vars[n].name, n)) {
190 		result = TRUE;
191 		break;
192 	    }
193 	}
194     }
195     return result;
196 }
197 
198 static void
free_cache(void)199 free_cache(void)
200 {
201     FreeAndNull(my_blob);
202     FreeAndNull(my_list);
203 }
204 
205 static void
update_tic_dir(const char * update)206 update_tic_dir(const char *update)
207 {
208     free((char *) TicDirectory);
209     TicDirectory = update;
210 }
211 
212 /*
213  * Record the "official" location of the terminfo directory, according to
214  * the place where we're writing to, or the normal default, if not.
215  */
216 NCURSES_EXPORT(const char *)
_nc_tic_dir(const char * path)217 _nc_tic_dir(const char *path)
218 {
219     T(("_nc_tic_dir %s", NonNull(path)));
220     if (!KeepTicDirectory) {
221 	if (path != NULL) {
222 	    if (path != TicDirectory)
223 		update_tic_dir(strdup(path));
224 	    HaveTicDirectory = TRUE;
225 	} else if (HaveTicDirectory == 0) {
226 	    if (use_terminfo_vars()) {
227 		const char *envp;
228 		if ((envp = getenv("TERMINFO")) != 0)
229 		    return _nc_tic_dir(envp);
230 	    }
231 	}
232     }
233     return TicDirectory ? TicDirectory : TERMINFO;
234 }
235 
236 /*
237  * Special fix to prevent the terminfo directory from being moved after tic
238  * has chdir'd to it.  If we let it be changed, then if $TERMINFO has a
239  * relative path, we'll lose track of the actual directory.
240  */
241 NCURSES_EXPORT(void)
_nc_keep_tic_dir(const char * path)242 _nc_keep_tic_dir(const char *path)
243 {
244     _nc_tic_dir(path);
245     KeepTicDirectory = TRUE;
246 }
247 
248 /*
249  * Cleanup.
250  */
251 NCURSES_EXPORT(void)
_nc_last_db(void)252 _nc_last_db(void)
253 {
254     if (my_blob != 0 && cache_expired()) {
255 	free_cache();
256     }
257 }
258 
259 /*
260  * This is a simple iterator which allows the caller to step through the
261  * possible locations for a terminfo directory.  ncurses uses this to find
262  * terminfo files to read.
263  */
264 NCURSES_EXPORT(const char *)
_nc_next_db(DBDIRS * state,int * offset)265 _nc_next_db(DBDIRS * state, int *offset)
266 {
267     const char *result;
268 
269     (void) offset;
270     if ((int) *state < my_size
271 	&& my_list != 0
272 	&& my_list[*state] != 0) {
273 	result = my_list[*state];
274 	(*state)++;
275     } else {
276 	result = 0;
277     }
278     if (result != 0) {
279 	T(("_nc_next_db %d %s", *state, result));
280     }
281     return result;
282 }
283 
284 NCURSES_EXPORT(void)
_nc_first_db(DBDIRS * state,int * offset)285 _nc_first_db(DBDIRS * state, int *offset)
286 {
287     bool cache_has_expired = FALSE;
288     *state = dbdTIC;
289     *offset = 0;
290 
291     T((T_CALLED("_nc_first_db")));
292 
293     /* build a blob containing all of the strings we will use for a lookup
294      * table.
295      */
296     if (my_blob == 0 || (cache_has_expired = cache_expired())) {
297 	size_t blobsize = 0;
298 	const char *values[dbdLAST];
299 	struct stat *my_stat;
300 	int j;
301 
302 	if (cache_has_expired)
303 	    free_cache();
304 
305 	for (j = 0; j < dbdLAST; ++j)
306 	    values[j] = 0;
307 
308 	/*
309 	 * This is the first item in the list, and is used only when tic is
310 	 * writing to the database, as a performance improvement.
311 	 */
312 	values[dbdTIC] = TicDirectory;
313 
314 #if NCURSES_USE_DATABASE
315 #ifdef TERMINFO_DIRS
316 	values[dbdCfgList] = TERMINFO_DIRS;
317 #endif
318 #ifdef TERMINFO
319 	values[dbdCfgOnce] = TERMINFO;
320 #endif
321 #endif
322 
323 #if NCURSES_USE_TERMCAP
324 	values[dbdCfgList2] = TERMPATH;
325 #endif
326 
327 	if (use_terminfo_vars()) {
328 #if NCURSES_USE_DATABASE
329 	    values[dbdEnvOnce] = cache_getenv("TERMINFO", dbdEnvOnce);
330 	    values[dbdHome] = _nc_home_terminfo();
331 	    (void) cache_getenv("HOME", dbdHome);
332 	    values[dbdEnvList] = cache_getenv("TERMINFO_DIRS", dbdEnvList);
333 
334 #endif
335 #if NCURSES_USE_TERMCAP
336 	    values[dbdEnvOnce2] = cache_getenv("TERMCAP", dbdEnvOnce2);
337 	    /* only use $TERMCAP if it is an absolute path */
338 	    if (values[dbdEnvOnce2] != 0
339 		&& *values[dbdEnvOnce2] != '/') {
340 		values[dbdEnvOnce2] = 0;
341 	    }
342 	    values[dbdEnvList2] = cache_getenv("TERMPATH", dbdEnvList2);
343 #endif /* NCURSES_USE_TERMCAP */
344 	}
345 
346 	for (j = 0; j < dbdLAST; ++j) {
347 	    if (values[j] == 0)
348 		values[j] = "";
349 	    blobsize += 2 + strlen(values[j]);
350 	}
351 
352 	my_blob = malloc(blobsize);
353 	if (my_blob != 0) {
354 	    *my_blob = '\0';
355 	    for (j = 0; j < dbdLAST; ++j) {
356 		add_to_blob(values[j], blobsize);
357 	    }
358 
359 	    /* Now, build an array which will be pointers to the distinct
360 	     * strings in the blob.
361 	     */
362 	    blobsize = 2;
363 	    for (j = 0; my_blob[j] != '\0'; ++j) {
364 		if (my_blob[j] == NCURSES_PATHSEP)
365 		    ++blobsize;
366 	    }
367 	    my_list = typeCalloc(char *, blobsize);
368 	    my_stat = typeCalloc(struct stat, blobsize);
369 	    if (my_list != 0 && my_stat != 0) {
370 		int k = 0;
371 		my_list[k++] = my_blob;
372 		for (j = 0; my_blob[j] != '\0'; ++j) {
373 		    if (my_blob[j] == NCURSES_PATHSEP
374 			&& ((&my_blob[j] - my_list[k - 1]) != 3
375 			    || !quick_prefix(my_list[k - 1]))) {
376 			my_blob[j] = '\0';
377 			my_list[k++] = &my_blob[j + 1];
378 		    }
379 		}
380 
381 		/*
382 		 * Eliminate duplicates from the list.
383 		 */
384 		for (j = 0; my_list[j] != 0; ++j) {
385 #ifdef TERMINFO
386 		    if (*my_list[j] == '\0') {
387 			char *my_copy = strdup(TERMINFO);
388 			if (my_copy != 0)
389 			    my_list[j] = my_copy;
390 		    }
391 #endif
392 		    trim_formatting(my_list[j]);
393 		    for (k = 0; k < j; ++k) {
394 			if (!strcmp(my_list[j], my_list[k])) {
395 			    T(("duplicate %s", my_list[j]));
396 			    k = j - 1;
397 			    while ((my_list[j] = my_list[j + 1]) != 0) {
398 				++j;
399 			    }
400 			    j = k;
401 			    break;
402 			}
403 		    }
404 		}
405 
406 		/*
407 		 * Eliminate non-existent databases, and those that happen to
408 		 * be symlinked to another location.
409 		 */
410 		for (j = 0; my_list[j] != 0; ++j) {
411 		    bool found = check_existence(my_list[j], &my_stat[j]);
412 #if HAVE_LINK
413 		    if (found) {
414 			for (k = 0; k < j; ++k) {
415 			    if (my_stat[j].st_dev == my_stat[k].st_dev
416 				&& my_stat[j].st_ino == my_stat[k].st_ino) {
417 				found = FALSE;
418 				break;
419 			    }
420 			}
421 		    }
422 #endif
423 		    if (!found) {
424 			T(("not found %s", my_list[j]));
425 			k = j;
426 			while ((my_list[k] = my_list[k + 1]) != 0) {
427 			    ++k;
428 			}
429 			--j;
430 		    }
431 		}
432 		my_size = j;
433 		my_time = time((time_t *) 0);
434 	    } else {
435 		FreeAndNull(my_blob);
436 	    }
437 	    free(my_stat);
438 	}
439     }
440     returnVoid;
441 }
442 
443 #if NO_LEAKS
444 void
_nc_db_iterator_leaks(void)445 _nc_db_iterator_leaks(void)
446 {
447     DBDIRS which;
448 
449     if (my_blob != 0)
450 	FreeAndNull(my_blob);
451     if (my_list != 0)
452 	FreeAndNull(my_list);
453     for (which = 0; (int) which < dbdLAST; ++which) {
454 	my_vars[which].name = 0;
455 	FreeIfNeeded(my_vars[which].value);
456 	my_vars[which].value = 0;
457     }
458     update_tic_dir(NULL);
459 }
460 #endif
461