1/*
2 *  $Id: rc.c,v 1.60 2020/11/25 00:06:40 tom Exp $
3 *
4 *  rc.c -- routines for processing the configuration file
5 *
6 *  Copyright 2000-2019,2020	Thomas E. Dickey
7 *
8 *  This program is free software; you can redistribute it and/or modify
9 *  it under the terms of the GNU Lesser General Public License, version 2.1
10 *  as published by the Free Software Foundation.
11 *
12 *  This program is distributed in the hope that it will be useful, but
13 *  WITHOUT ANY WARRANTY; without even the implied warranty of
14 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15 *  Lesser General Public License for more details.
16 *
17 *  You should have received a copy of the GNU Lesser General Public
18 *  License along with this program; if not, write to
19 *	Free Software Foundation, Inc.
20 *	51 Franklin St., Fifth Floor
21 *	Boston, MA 02110, USA.
22 *
23 *  An earlier version of this program lists as authors
24 *	Savio Lam (lam836@cs.cuhk.hk)
25 */
26
27#include <dialog.h>
28
29#include <dlg_keys.h>
30
31#ifdef HAVE_COLOR
32#include <dlg_colors.h>
33#include <dlg_internals.h>
34
35#define L_PAREN '('
36#define R_PAREN ')'
37
38#define MIN_TOKEN 3
39#ifdef HAVE_RC_FILE2
40#define MAX_TOKEN 5
41#else
42#define MAX_TOKEN MIN_TOKEN
43#endif
44
45#define UNKNOWN_COLOR -2
46
47/*
48 * For matching color names with color values
49 */
50static const color_names_st color_names[] =
51{
52#ifdef HAVE_USE_DEFAULT_COLORS
53    {"DEFAULT", -1},
54#endif
55    {"BLACK", COLOR_BLACK},
56    {"RED", COLOR_RED},
57    {"GREEN", COLOR_GREEN},
58    {"YELLOW", COLOR_YELLOW},
59    {"BLUE", COLOR_BLUE},
60    {"MAGENTA", COLOR_MAGENTA},
61    {"CYAN", COLOR_CYAN},
62    {"WHITE", COLOR_WHITE},
63};				/* color names */
64#define COLOR_COUNT     TableSize(color_names)
65#endif /* HAVE_COLOR */
66
67#define GLOBALRC "/etc/dialogrc"
68#define DIALOGRC ".dialogrc"
69
70/* Types of values */
71#define VAL_INT  0
72#define VAL_STR  1
73#define VAL_BOOL 2
74
75/* Type of line in configuration file */
76typedef enum {
77    LINE_ERROR = -1,
78    LINE_EQUALS,
79    LINE_EMPTY
80} PARSE_LINE;
81
82/* number of configuration variables */
83#define VAR_COUNT        TableSize(vars)
84
85/* check if character is string quoting characters */
86#define isquote(c)       ((c) == '"' || (c) == '\'')
87
88/* get last character of string */
89#define lastch(str)      str[strlen(str)-1]
90
91/*
92 * Configuration variables
93 */
94typedef struct {
95    const char *name;		/* name of configuration variable as in DIALOGRC */
96    void *var;			/* address of actual variable to change */
97    int type;			/* type of value */
98    const char *comment;	/* comment to put in "rc" file */
99} vars_st;
100
101/*
102 * This table should contain only references to dialog_state, since dialog_vars
103 * is reset specially in dialog.c before each widget.
104 */
105static const vars_st vars[] =
106{
107    {"aspect",
108     &dialog_state.aspect_ratio,
109     VAL_INT,
110     "Set aspect-ration."},
111
112    {"separate_widget",
113     &dialog_state.separate_str,
114     VAL_STR,
115     "Set separator (for multiple widgets output)."},
116
117    {"tab_len",
118     &dialog_state.tab_len,
119     VAL_INT,
120     "Set tab-length (for textbox tab-conversion)."},
121
122    {"visit_items",
123     &dialog_state.visit_items,
124     VAL_BOOL,
125     "Make tab-traversal for checklist, etc., include the list."},
126
127#ifdef HAVE_COLOR
128    {"use_shadow",
129     &dialog_state.use_shadow,
130     VAL_BOOL,
131     "Shadow dialog boxes? This also turns on color."},
132
133    {"use_colors",
134     &dialog_state.use_colors,
135     VAL_BOOL,
136     "Turn color support ON or OFF"},
137#endif				/* HAVE_COLOR */
138};				/* vars */
139
140static int
141skip_whitespace(char *str, int n)
142{
143    while (isblank(UCH(str[n])) && str[n] != '\0')
144	n++;
145    return n;
146}
147
148static int
149skip_keyword(char *str, int n)
150{
151    while (isalnum(UCH(str[n])) && str[n] != '\0')
152	n++;
153    return n;
154}
155
156static void
157trim_token(char **tok)
158{
159    char *tmp = *tok + skip_whitespace(*tok, 0);
160
161    *tok = tmp;
162
163    while (*tmp != '\0' && !isblank(UCH(*tmp)))
164	tmp++;
165
166    *tmp = '\0';
167}
168
169static int
170from_boolean(const char *str)
171{
172    int code = -1;
173
174    if (str != NULL && *str != '\0') {
175	if (!dlg_strcmp(str, "ON")) {
176	    code = 1;
177	} else if (!dlg_strcmp(str, "OFF")) {
178	    code = 0;
179	}
180    }
181    return code;
182}
183
184static int
185from_color_name(const char *str)
186{
187    int code = UNKNOWN_COLOR;
188
189    if (str != NULL && *str != '\0') {
190	size_t i;
191
192	for (i = 0; i < COLOR_COUNT; ++i) {
193	    if (!dlg_strcmp(str, color_names[i].name)) {
194		code = color_names[i].value;
195		break;
196	    }
197	}
198    }
199    return code;
200}
201
202static int
203find_vars(char *name)
204{
205    int result = -1;
206    unsigned i;
207
208    for (i = 0; i < VAR_COUNT; i++) {
209	if (dlg_strcmp(vars[i].name, name) == 0) {
210	    result = (int) i;
211	    break;
212	}
213    }
214    return result;
215}
216
217#ifdef HAVE_COLOR
218static int
219find_color(char *name)
220{
221    int result = -1;
222    int i;
223    int limit = dlg_color_count();
224
225    for (i = 0; i < limit; i++) {
226	if (dlg_strcmp(dlg_color_table[i].name, name) == 0) {
227	    result = i;
228	    break;
229	}
230    }
231    return result;
232}
233
234static const char *
235to_color_name(int code)
236{
237    const char *result = "?";
238    size_t n;
239    for (n = 0; n < TableSize(color_names); ++n) {
240	if (code == color_names[n].value) {
241	    result = color_names[n].name;
242	    break;
243	}
244    }
245    return result;
246}
247
248static const char *
249to_boolean(int code)
250{
251    return code ? "ON" : "OFF";
252}
253
254/*
255 * Extract the foreground, background and highlight values from an attribute
256 * represented as a string in one of these forms:
257 *
258 * "(foreground,background,highlight,underline,reverse)"
259 * "(foreground,background,highlight,underline)"
260 * "(foreground,background,highlight)"
261 * "xxxx_color"
262 */
263static int
264str_to_attr(char *str, DIALOG_COLORS * result)
265{
266    char *tokens[MAX_TOKEN + 1];
267    char tempstr[MAX_LEN + 1];
268    size_t have;
269    size_t i = 0;
270    size_t tok_count = 0;
271
272    memset(result, 0, sizeof(*result));
273    result->fg = -1;
274    result->bg = -1;
275    result->hilite = -1;
276
277    if (str[0] != L_PAREN || lastch(str) != R_PAREN) {
278	int ret;
279
280	if ((ret = find_color(str)) >= 0) {
281	    *result = dlg_color_table[ret];
282	    return 0;
283	}
284	/* invalid representation */
285	return -1;
286    }
287
288    /* remove the parenthesis */
289    have = strlen(str);
290    if (have > MAX_LEN) {
291	have = MAX_LEN - 1;
292    } else {
293	have -= 2;
294    }
295    memcpy(tempstr, str + 1, have);
296    tempstr[have] = '\0';
297
298    /* parse comma-separated tokens, allow up to
299     * one more than max tokens to detect extras */
300    while (tok_count < TableSize(tokens)) {
301
302	tokens[tok_count++] = &tempstr[i];
303
304	while (tempstr[i] != '\0' && tempstr[i] != ',')
305	    i++;
306
307	if (tempstr[i] == '\0')
308	    break;
309
310	tempstr[i++] = '\0';
311    }
312
313    if (tok_count < MIN_TOKEN || tok_count > MAX_TOKEN) {
314	/* invalid representation */
315	return -1;
316    }
317
318    for (i = 0; i < tok_count; ++i)
319	trim_token(&tokens[i]);
320
321    /* validate */
322    if (UNKNOWN_COLOR == (result->fg = from_color_name(tokens[0]))
323	|| UNKNOWN_COLOR == (result->bg = from_color_name(tokens[1]))
324	|| UNKNOWN_COLOR == (result->hilite = from_boolean(tokens[2]))
325#ifdef HAVE_RC_FILE2
326	|| (tok_count >= 4 && (result->ul = from_boolean(tokens[3])) == -1)
327	|| (tok_count >= 5 && (result->rv = from_boolean(tokens[4])) == -1)
328#endif /* HAVE_RC_FILE2 */
329	) {
330	/* invalid representation */
331	return -1;
332    }
333
334    return 0;
335}
336#endif /* HAVE_COLOR */
337
338/*
339 * Check if the line begins with a special keyword; if so, return true while
340 * pointing params to its parameters.
341 */
342static int
343begins_with(char *line, const char *keyword, char **params)
344{
345    int i = skip_whitespace(line, 0);
346    int j = skip_keyword(line, i);
347
348    if ((j - i) == (int) strlen(keyword)) {
349	char save = line[j];
350	line[j] = 0;
351	if (!dlg_strcmp(keyword, line + i)) {
352	    *params = line + skip_whitespace(line, j + 1);
353	    return 1;
354	}
355	line[j] = save;
356    }
357
358    return 0;
359}
360
361/*
362 * Parse a line in the configuration file
363 *
364 * Each line is of the form:  "variable = value". On exit, 'var' will contain
365 * the variable name, and 'value' will contain the value string.
366 *
367 * Return values:
368 *
369 * LINE_EMPTY   - line is blank or comment
370 * LINE_EQUALS  - line contains "variable = value"
371 * LINE_ERROR   - syntax error in line
372 */
373static PARSE_LINE
374parse_line(char *line, char **var, char **value)
375{
376    int i = 0;
377
378    /* ignore white space at beginning of line */
379    i = skip_whitespace(line, i);
380
381    if (line[i] == '\0')	/* line is blank */
382	return LINE_EMPTY;
383    else if (line[i] == '#')	/* line is comment */
384	return LINE_EMPTY;
385    else if (line[i] == '=')	/* variable names cannot start with a '=' */
386	return LINE_ERROR;
387
388    /* set 'var' to variable name */
389    *var = line + i++;		/* skip to next character */
390
391    /* find end of variable name */
392    while (!isblank(UCH(line[i])) && line[i] != '=' && line[i] != '\0')
393	i++;
394
395    if (line[i] == '\0')	/* syntax error */
396	return LINE_ERROR;
397    else if (line[i] == '=')
398	line[i++] = '\0';
399    else {
400	line[i++] = '\0';
401
402	/* skip white space before '=' */
403	i = skip_whitespace(line, i);
404
405	if (line[i] != '=')	/* syntax error */
406	    return LINE_ERROR;
407	else
408	    i++;		/* skip the '=' */
409    }
410
411    /* skip white space after '=' */
412    i = skip_whitespace(line, i);
413
414    if (line[i] == '\0')
415	return LINE_ERROR;
416    else
417	*value = line + i;	/* set 'value' to value string */
418
419    /* trim trailing white space from 'value' */
420    i = (int) strlen(*value) - 1;
421    while (isblank(UCH((*value)[i])) && i > 0)
422	i--;
423    (*value)[i + 1] = '\0';
424
425    return LINE_EQUALS;		/* no syntax error in line */
426}
427
428/*
429 * Create the configuration file
430 */
431void
432dlg_create_rc(const char *filename)
433{
434    unsigned i;
435    FILE *rc_file;
436
437    if ((rc_file = fopen(filename, "wt")) == NULL)
438	dlg_exiterr("Error opening file for writing in dlg_create_rc().");
439
440    fprintf(rc_file, "#\n\
441# Run-time configuration file for dialog\n\
442#\n\
443# Automatically generated by \"dialog --create-rc <file>\"\n\
444#\n\
445#\n\
446# Types of values:\n\
447#\n\
448# Number     -  <number>\n\
449# String     -  \"string\"\n\
450# Boolean    -  <ON|OFF>\n"
451#ifdef HAVE_COLOR
452#ifdef HAVE_RC_FILE2
453	    "\
454# Attribute  -  (foreground,background,highlight?,underline?,reverse?)\n"
455#else /* HAVE_RC_FILE2 */
456	    "\
457# Attribute  -  (foreground,background,highlight?)\n"
458#endif /* HAVE_RC_FILE2 */
459#endif /* HAVE_COLOR */
460	);
461
462    /* Print an entry for each configuration variable */
463    for (i = 0; i < VAR_COUNT; i++) {
464	fprintf(rc_file, "\n# %s\n", vars[i].comment);
465	switch (vars[i].type) {
466	case VAL_INT:
467	    fprintf(rc_file, "%s = %d\n", vars[i].name,
468		    *((int *) vars[i].var));
469	    break;
470	case VAL_STR:
471	    fprintf(rc_file, "%s = \"%s\"\n", vars[i].name,
472		    (char *) vars[i].var);
473	    break;
474	case VAL_BOOL:
475	    fprintf(rc_file, "%s = %s\n", vars[i].name,
476		    *((bool *) vars[i].var) ? "ON" : "OFF");
477	    break;
478	}
479    }
480#ifdef HAVE_COLOR
481    for (i = 0; i < (unsigned) dlg_color_count(); ++i) {
482	unsigned j;
483	bool repeat = FALSE;
484
485	fprintf(rc_file, "\n# %s\n", dlg_color_table[i].comment);
486	for (j = 0; j != i; ++j) {
487	    if (dlg_color_table[i].fg == dlg_color_table[j].fg
488		&& dlg_color_table[i].bg == dlg_color_table[j].bg
489		&& dlg_color_table[i].hilite == dlg_color_table[j].hilite) {
490		fprintf(rc_file, "%s = %s\n",
491			dlg_color_table[i].name,
492			dlg_color_table[j].name);
493		repeat = TRUE;
494		break;
495	    }
496	}
497
498	if (!repeat) {
499	    fprintf(rc_file, "%s = %c", dlg_color_table[i].name, L_PAREN);
500	    fprintf(rc_file, "%s", to_color_name(dlg_color_table[i].fg));
501	    fprintf(rc_file, ",%s", to_color_name(dlg_color_table[i].bg));
502	    fprintf(rc_file, ",%s", to_boolean(dlg_color_table[i].hilite));
503#ifdef HAVE_RC_FILE2
504	    if (dlg_color_table[i].ul || dlg_color_table[i].rv)
505		fprintf(rc_file, ",%s", to_boolean(dlg_color_table[i].ul));
506	    if (dlg_color_table[i].rv)
507		fprintf(rc_file, ",%s", to_boolean(dlg_color_table[i].rv));
508#endif /* HAVE_RC_FILE2 */
509	    fprintf(rc_file, "%c\n", R_PAREN);
510	}
511    }
512#endif /* HAVE_COLOR */
513    dlg_dump_keys(rc_file);
514
515    (void) fclose(rc_file);
516}
517
518static void
519report_error(const char *filename, int line_no, const char *msg)
520{
521    fprintf(stderr, "%s:%d: %s\n", filename, line_no, msg);
522    dlg_trace_msg("%s:%d: %s\n", filename, line_no, msg);
523}
524
525/*
526 * Parse the configuration file and set up variables
527 */
528int
529dlg_parse_rc(void)
530{
531    int i;
532    int l = 1;
533    PARSE_LINE parse;
534    char str[MAX_LEN + 1];
535    char *var;
536    char *value;
537    char *filename;
538    int result = 0;
539    FILE *rc_file = 0;
540    char *params;
541
542    /*
543     *  At startup, dialog determines the settings to use as follows:
544     *
545     *  a) if the environment variable $DIALOGRC is set, its value determines
546     *     the name of the configuration file.
547     *
548     *  b) if the file in (a) can't be found, use the file $HOME/.dialogrc
549     *     as the configuration file.
550     *
551     *  c) if the file in (b) can't be found, try using the GLOBALRC file.
552     *     Usually this will be /etc/dialogrc.
553     *
554     *  d) if the file in (c) cannot be found, use the compiled-in defaults.
555     */
556
557    /* try step (a) */
558    if ((filename = dlg_getenv_str("DIALOGRC")) != NULL)
559	rc_file = fopen(filename, "rt");
560
561    if (rc_file == NULL) {	/* step (a) failed? */
562	/* try step (b) */
563	if ((filename = dlg_getenv_str("HOME")) != NULL
564	    && strlen(filename) < MAX_LEN - (sizeof(DIALOGRC) + 3)) {
565	    if (filename[0] == '\0' || lastch(filename) == '/')
566		sprintf(str, "%s%s", filename, DIALOGRC);
567	    else
568		sprintf(str, "%s/%s", filename, DIALOGRC);
569	    rc_file = fopen(filename = str, "rt");
570	}
571    }
572
573    if (rc_file == NULL) {	/* step (b) failed? */
574	/* try step (c) */
575	strcpy(str, GLOBALRC);
576	if ((rc_file = fopen(filename = str, "rt")) == NULL)
577	    return 0;		/* step (c) failed, use default values */
578    }
579
580    DLG_TRACE(("# opened rc file \"%s\"\n", filename));
581    /* Scan each line and set variables */
582    while ((result == 0) && (fgets(str, MAX_LEN, rc_file) != NULL)) {
583	DLG_TRACE(("#\t%s", str));
584	if (*str == '\0' || lastch(str) != '\n') {
585	    /* ignore rest of file if line too long */
586	    report_error(filename, l, "line too long");
587	    result = -1;	/* parse aborted */
588	    break;
589	}
590
591	lastch(str) = '\0';
592	if (begins_with(str, "bindkey", &params)) {
593	    if (!dlg_parse_bindkey(params)) {
594		report_error(filename, l, "invalid bindkey");
595		result = -1;
596	    }
597	    continue;
598	}
599	parse = parse_line(str, &var, &value);	/* parse current line */
600
601	switch (parse) {
602	case LINE_EMPTY:	/* ignore blank lines and comments */
603	    break;
604	case LINE_EQUALS:
605	    /* search table for matching config variable name */
606	    if ((i = find_vars(var)) >= 0) {
607		switch (vars[i].type) {
608		case VAL_INT:
609		    *((int *) vars[i].var) = atoi(value);
610		    break;
611		case VAL_STR:
612		    if (!isquote(value[0]) || !isquote(lastch(value))
613			|| strlen(value) < 2) {
614			report_error(filename, l, "expected string value");
615			result = -1;	/* parse aborted */
616		    } else {
617			/* remove the (") quotes */
618			value++;
619			lastch(value) = '\0';
620			strcpy((char *) vars[i].var, value);
621		    }
622		    break;
623		case VAL_BOOL:
624		    if (!dlg_strcmp(value, "ON"))
625			*((bool *) vars[i].var) = TRUE;
626		    else if (!dlg_strcmp(value, "OFF"))
627			*((bool *) vars[i].var) = FALSE;
628		    else {
629			report_error(filename, l, "expected boolean value");
630			result = -1;	/* parse aborted */
631		    }
632		    break;
633		}
634#ifdef HAVE_COLOR
635	    } else if ((i = find_color(var)) >= 0) {
636		DIALOG_COLORS temp;
637		if (str_to_attr(value, &temp) == -1) {
638		    report_error(filename, l, "expected attribute value");
639		    result = -1;	/* parse aborted */
640		} else {
641		    dlg_color_table[i].fg = temp.fg;
642		    dlg_color_table[i].bg = temp.bg;
643		    dlg_color_table[i].hilite = temp.hilite;
644#ifdef HAVE_RC_FILE2
645		    dlg_color_table[i].ul = temp.ul;
646		    dlg_color_table[i].rv = temp.rv;
647#endif /* HAVE_RC_FILE2 */
648		}
649	    } else {
650#endif /* HAVE_COLOR */
651		report_error(filename, l, "unknown variable");
652		result = -1;	/* parse aborted */
653	    }
654	    break;
655	case LINE_ERROR:
656	    report_error(filename, l, "syntax error");
657	    result = -1;	/* parse aborted */
658	    break;
659	}
660	l++;			/* next line */
661    }
662
663    (void) fclose(rc_file);
664    return result;
665}
666